Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Future of Jira Software powered by Kotlin - Kotlin Night Ankara

Future of Jira Software powered by Kotlin - Kotlin Night Ankara

Atlassian is on a decomposition journey. We split our monoliths in microservices and we build our microservices in Kotlin. Our services are reactive and resilient and working on them is kool.

This is a story of Jira Software’s transition to Kotlin. How we built a GraphQL gateway in Kotlin with Spring Boot and Project Reactor. Our success in interoperability with Java and how we solved issues when crossing the boundary between the reactive and non-reactive world. It also explains how Kotlin helped our developers become more productive and how resilience helps us build better services for consumers.

Martin T. Varga

March 06, 2020
Tweet

More Decks by Martin T. Varga

Other Decks in Programming

Transcript

  1. View Slide

  2. MARTIN VARGA | SOFTWARE DEVELOPER | @MARTINTEEVARGA
    Future of Jira Software
    Powered by Kotlin

    View Slide

  3. View Slide

  4. kotlinlang.org

    View Slide

  5. View Slide

  6. Google
    Pixel 2
    Just Black
    Simply drop
    your screenshot
    on to this
    media
    placeholder
    TRELLO MOBILE

    View Slide

  7. Jira
    For cloud and server
    Confluence
    For cloud and server
    OpsGenie
    Our recent acquisition
    Not just Trello

    View Slide

  8. Not just mobile!

    View Slide

  9. FUTURE OF JIRA SOFTWARE
    POWERED BY KOTLIN

    View Slide

  10. JIRA SOFTWARE?

    View Slide

  11. Plan, track and
    release software.
    JIRA SOFTWARE

    View Slide

  12. View Slide

  13. The Story of Jira
    From the developer’s perspective

    View Slide

  14. Server Cloud
    1001001
    0 0 1 0 0 1
    0 0 1 0 0 1
    1101010
    1010101
    0 0 1 0 0 1
    1110001001
    0011110010
    1100101011
    01111110001
    Single Code Base

    View Slide

  15. Server Cloud
    1001001
    0 0 1 0 0 1
    0 0 1 0 0 1
    1101010
    1010101
    0 0 1 0 0 1

    View Slide

  16. 1101010
    1010101
    0 0 1 0 0 1
    1101010
    1010101
    0 0 1 0 0 1

    View Slide

  17. View Slide

  18. 1101010
    1010101
    0 0 1 0 0 1

    View Slide

  19. 11010101010101
    0010010101001
    00101010111010
    1010101010010
    0101010010010
    10101110101010

    View Slide

  20. 110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101

    View Slide

  21. View Slide

  22. 110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101

    View Slide

  23. 110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101

    View Slide

  24. 110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101
    010010010101001001010101
    110101010101010010010101
    001001010101110101010101

    View Slide

  25. 1101010101010100
    1001010100100101
    0101110101010101
    010010010101001
    0010101011101010
    1010101001001010
    1001001010101110
    1 0 1 0 1 0
    1010111
    001001

    View Slide

  26. 1101010101
    010100100
    101010010
    0101010111
    0 1 0 1 0 1
    1011100
    1 0 0 1 0 1
    1 0 1 0 1 0
    1010111
    001001

    View Slide

  27. 1101010
    1010101
    001001
    0 1 0 1 0 1
    1011100
    1 0 0 1 0 1
    1 0 1 0 1 0
    1010111
    001001
    0 1 0 1 0 1
    1011100
    1 0 0 1 0 1

    View Slide

  28. 1101010
    1010101
    001001
    0 1 0 1 0 1
    1011100
    1 0 0 1 0 1
    1 0 1 0 1 0
    1010111
    001001
    0 1 0 1 0 1
    1011100
    1 0 0 1 0 1

    View Slide

  29. 1101010
    1010101
    001001
    0 1 0 1 0 1
    1011100
    1 0 0 1 0 1

    View Slide

  30. View Slide

  31. How Atlassian builds services
    Making developers productive with platform as a service

    View Slide

  32. PaaS that allows
    devs to create and
    deploy a service in
    matter of minutes.
    MICROS

    View Slide

  33. MICROS

    View Slide

  34. name: "animalfacts"
    compose:
    animalfacts:
    image: internal.docker.repo/animalfacts
    tag: 'PROJECT_VERSION'
    ports:
    - 8080:8080
    MICROS DESCRIPTOR

    View Slide

  35. links:
    healthcheck:
    uri: heartbeat
    port: 8080
    source:
    url: "[email protected]:atlassianlabs/animal-facts-
    graphql-gateway-demo.git"
    notifications:
    email: "[email protected]"
    resources:
    - type: dynamo-db
    name: facts
    attributes:
    ReadWriteCapacityMode: ON_DEMAND

    View Slide

  36. Jira Software Tech Stack
    DynamoDB or PostgreSQL
    Depends on the application.
    Project Reactor
    Mature reactive library.
    Kotlin
    For all new services.
    Spring Boot
    Custom wrapper that makes
    monitoring etc. easier.

    View Slide

  37. Powered by Kotlin
    Making developers happy

    View Slide

  38. WHY KOTLIN?

    View Slide

  39. Experience
    Have we learned Kotlin enough? Was it a
    success?
    Compatibility
    How well is it going to play with our current tech
    stacks?
    Staffing
    Are we going to have enough Kotlin developers
    available?
    Adopting
    Kotlin

    View Slide

  40. Velocity
    Does it make our developers more productive?
    Industry
    Is it already adopted by the developers
    community?
    Adopting
    Kotlin

    View Slide


  41. View Slide

  42. Experienced with Kotlin.
    Android Developer Ad

    View Slide

  43. Experience or interest in Kotlin
    development.
    Software Engineer, Developer Productivity, Jira Cloud Ad

    View Slide

  44. We happen to have a variety of
    languages within our stack
    including Kotlin, Python, and
    Ruby.
    Senior Backend Developer Ad

    View Slide

  45. A case for a reactive app
    A real world example of an app that we built recently

    View Slide

  46. LATENCY
    250ms
    250ms
    250ms
    750ms

    View Slide

  47. LATENCY
    10ms
    250ms
    10ms
    10ms
    280ms

    View Slide

  48. Operators
    Useful for modifying and combining sources.
    Allows composition of libraries that add
    resilience, tracing etc.
    Async
    Calls a service, then does mostly nothing and
    waits. Perfect fit.
    Project
    reactor
    Clean code
    No callback hell.

    View Slide

  49. val currentBoard = “5”.toMono()
    MONO
    5

    View Slide

  50. val recentBoards = listOf("0", "1", "2").toFlux()
    FLUX
    0 1 2

    View Slide

  51. val currentBoard = "5".toMono()
    val recentBoards = listOf("0", "1", "2").toFlux()
    currentBoard.concatWith(recentBoards)
    COMPOSITION & CLEAN CODE
    5
    0 1 2
    0 1 2
    5

    View Slide

  52. val currentBoard = "5".toMono()
    val recentBoards = listOf("0", "1", "2").toFlux()
    currentBoard.concatWith(recentBoards)
    .map {
    boardService.getName(it)
    }
    .take(3)
    .subscribe {
    println(it)
    }
    COMPOSITION & CLEAN CODE

    View Slide

  53. Surfboard
    Skateboard
    Ironing board
    COMPOSITION & CLEAN CODE

    View Slide

  54. currentBoard.concatWith(recentBoards)
    .map {
    boardService.getName(it)
    }
    .timeout(Duration.ofMillis(2500))
    .onErrorReturn("Recent board”)
    .take(3)
    .subscribe {
    println(it)
    }
    COMPOSITION & CLEAN CODE

    View Slide

  55. Surfboard
    Recent board
    Ironing board
    COMPOSITION & CLEAN CODE

    View Slide

  56. val currentBoard = currentBoardServiceMono()
    val recentBoards = recentBoardsServiceFlux()
    return currentBoard.concatWith(recentBoards)
    .map {
    boardService.getName(it)
    }
    .timeout(Duration.ofMillis(2500))
    .onErrorReturn("Recent board”)
    .take(3)
    REAL WORLD EXAMPLE

    View Slide

  57. ThreadLocal value = new ThreadLocal<>();
    value.set(42);

    Integer integer = value.get();
    REACTOR GOTCHAS

    View Slide

  58. currentUser.concatWith(recentUsers)
    .subscriberContext { sc ->
    sc.put("key", value)
    }
    .flatMap { userId ->
    Mono.subscriberContext().map { sc ->
    userService.getFullName(userId, sc.get("key"))
    }
    }
    .subscribe {
    println(it)
    }
    SUBSCRIBER CONTEXT

    View Slide

  59. Get exactly what you want
    Query multiple sources at once, specify what you
    want.
    Strongly typed schema
    Schema is a living documentation.
    Mature web clients
    Clients like Apollo let us remove dependency on
    Redux in ReactJs web apps.
    GraphQL

    View Slide

  60. type Query {
    board(id: ID): Board
    }
    type Board {
    id: ID
    name: String
    issues: [Issue]
    }
    type Issue {
    id: ID
    key: String
    }
    SCHEMA

    View Slide

  61. POST /jsw/graphql
    query {
    board(id: 42) {
    name
    issues {
    key
    }
    }
    }
    QUERY

    View Slide

  62. {
    "data": {
    "board": {
    "name": "Software board”
    "issues": [
    {
    "key": "KEY-1"
    }
    ]
    }
    }
    }
    RESPONSE

    View Slide

  63. Project Reactor + GraphQL Java
    OUR SOLUTION

    View Slide

  64. DataFetching
    Environment
    Subscriber
    Context
    PROJECT REACTOR GRAPHQL-JAVA

    View Slide

  65. Let’s write some code
    GraphQL gateway, Jira Software style

    View Slide

  66. GET https://catfact.ninja/fact
    {
    "fact": "The leopard is the most widespread of all big
    cats.",
    "length": 51
    }
    CAT FACTS API

    View Slide

  67. GET https://some-random-api.ml/facts/dog
    {
    "fact": "A dogs' first sense to develop is touch."
    }
    DOG FACTS API

    View Slide

  68. Cat Facts
    Dog Facts

    View Slide

  69. WHY NOT BOTH?

    View Slide

  70. POST https://animalfactsgateway.com/grapqhl
    query {
    dog {
    fact
    length
    }
    cat {
    fact
    length
    }
    }
    ANIMAL FACTS GRAPHQL API

    View Slide

  71. {
    "data": {
    "dog": {
    "fact": "Dog fact",
    "length": 8
    },
    "cat": {
    "fact": "Cat fact",
    "length": 8
    }
    }
    }

    View Slide

  72. dependencies {
    implementation("org.springframework.boot:spring-boot-
    starter-webflux")
    implementation("com.fasterxml.jackson.module:jackson-
    module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("com.graphql-java:graphql-java:11.0")
    }

    View Slide

  73. package com.atlassian.jsw.factsdemo
    import o.s.b.autoconfigure.SpringBootApplication
    import o.s.b.runApplication
    @SpringBootApplication
    class DemoApplication
    fun main(args: Array) {
    runApplication(*args)
    }
    APPLICATION

    View Slide

  74. type Query {
    cat: Fact
    dog: Fact
    }
    type Fact {
    fact: String
    length: Int
    }
    GRAPHQL SCHEMA

    View Slide

  75. interface FactFetcher: DataFetcher>
    FETCHERS
    public interface DataFetcher {
    T get(DataFetchingEnvironment env) throws Exception;
    }

    View Slide

  76. data class Fact(val fact: String, val length: Int)
    FACT CLASS

    View Slide

  77. @Component
    class CatFactFetcher(val webClient: WebClient) :
    FactFetcher {
    override fun get(environment:
    DataFetchingEnvironment): CompletionStage =
    webClient.get().uri("https://catfact.ninja/fact")
    .retrieve()
    .bodyToMono(CatFact::class.java)
    .map { Fact(it.fact, it.length) }
    .toFuture()
    }
    data class CatFact(val fact: String, val length: Int)
    CAT FACTS FETCHER

    View Slide

  78. @Component
    class DogFactFetcher(val webClient: WebClient) :
    FactFetcher {
    override fun get(environment:
    DataFetchingEnvironment): CompletionStage =
    webClient.get().uri("https://.../facts/dog")
    .retrieve()
    .bodyToMono(DogFact::class.java)
    .map { Fact(it.fact, it.fact.length) }
    .toFuture()
    }
    data class DogFact(val fact: String)
    DOG FACTS FETCHER

    View Slide

  79. @Configuration
    class GraphqlConfig {
    @Bean
    fun schema(wiring: RuntimeWiring): GraphQLSchema =
    SchemaGenerator().makeExecutableSchema(
    parseSchema(“/graphql/schema.graphqls"),
    wiring
    )
    }
    GRAPHQL CONFIGURATION

    View Slide

  80. @Configuration
    class GraphqlConfig {
    @Bean
    fun factsWiring(
    dogFactFetcher: DogFactFetcher,
    catFactFetcher: CatFactFetcher
    ): RuntimeWiring =
    RuntimeWiring.newRuntimeWiring()
    .type(
    newTypeWiring("Query")
    .dataFetcher("dog", dogFactFetcher)
    .dataFetcher("cat", catFactFetcher)
    )
    .build()
    }

    View Slide

  81. override fun processQuery(
    query: String
    ): Mono = Mono.fromFuture(
    GraphQL
    .newGraphQL(graphQLSchema)
    .build()
    .executeAsync(
    ExecutionInput.newExecutionInput()
    .query(query)
    .build()
    )
    )
    SERVICE METHOD

    View Slide

  82. @PostMapping(
    path = ["/graphql"],
    consumes = [MediaType.APPLICATION_JSON_VALUE],
    produces = [MediaType.APPLICATION_JSON_VALUE]
    )
    fun graphql(
    @RequestBody body: Map
    ): Mono> {
    val query: String = body["query"] as String? ?: "{}"
    return graphQLService.processQuery(query)
    .map { it.toSpecification() }
    }
    REACTIVE API

    View Slide

  83. {
    "data": {
    "dog": {
    "fact": "“Him” and “Her” were the names of President
    Lyndon Johnson's two beagles.",
    "length": 73
    },
    "cat": {
    "fact": "Jaguars are the only big cats that don't
    roar.",
    "length": 46
    }
    }
    }
    RESULT

    View Slide

  84. Resilience
    Protect the downstream services from the upstream.

    View Slide


  85. 250ms 380ms
    380ms

    View Slide

  86. CAT FACTS BROKEN



    250ms 30s
    30s

    View Slide

  87. {
    "data": {
    "dog": {…},
    "cat": null
    },
    "errors": [
    {
    "message": "Exception while fetching data (/cat) :
    Operation timed out",
    "path": [
    "cat"
    ]
    }
    ]
    }

    View Slide

  88. TIMEOUT



    250ms 5000ms
    5000ms

    View Slide

  89. class CatFactFetcher(
    val webClient: WebClient
    ) : FactFetcher {
    override fun get(environment:
    DataFetchingEnvironment): CompletionStage =
    webClient.get().uri("https://catfact.ninja/fact")
    .retrieve()
    .bodyToMono(CatFact::class.java)
    .map { … }
    .timeout(Duration.ofMillis(5000))
    .toFuture()
    }
    CAT FACTS WITH TIMEOUT

    View Slide

  90. {
    "data": {
    "dog": {…},
    "cat": null
    },
    "errors": [
    {
    "message": "Exception while fetching data (/cat) :
    Did not observe any item or terminal signal within 5000ms
    in 'map' (and no fallback has been configured)",
    "path": [
    “cat"
    ]
    }
    ]
    }

    View Slide

  91. CIRCUIT BREAKER



    250ms 0ms
    250ms

    View Slide

  92. CIRCUIT BREAKER
    Closed
    Open
    Half-open


    View Slide

  93. class CatFactFetcher(
    val webClient: WebClient,
    val catBreaker: CircuitBreaker
    ) : FactFetcher {
    override fun get(environment:
    DataFetchingEnvironment): CompletionStage =
    webClient.get()


    .timeout(Duration.ofMillis(2500))
    .transform(
    CircuitBreakerOperator.of(catBreaker)
    )
    .toFuture()
    }
    CAT FACTS FETCHER WITH CIRCUIT BREAKER

    View Slide

  94. @Bean
    fun catCircuitBreaker(): CircuitBreaker {
    val circuitBreakerConfig =
    CircuitBreakerConfig.custom()
    .ringBufferSizeInClosedState(3)
    .ringBufferSizeInHalfOpenState(3)
    .waitDurationInOpenState(Duration.ofMillis(5000))
    .failureRateThreshold(0.5f)
    .build()
    return circuitBreakerRegistry.circuitBreaker(
    "CatCCB",
    circuitBreakerConfig
    )
    }

    View Slide

  95. {
    "data": {
    "dog": {…},
    "cat": null
    },
    "errors": [
    {
    "message": "Exception while fetching data (/cat) :
    CircuitBreaker 'CatCCB' is OPEN and does not permit
    further calls",
    "path": [
    "cat"
    ]
    }
    ]
    }

    View Slide

  96. CIRCUIT BREAKER



    250ms 0ms
    250ms
    But we might miss some cat facts.

    View Slide

  97. Takeaways
    Even if you forget everything else I said, please try…

    View Slide

  98. GraphQL
    For APIs consumed from the front-end.
    Resilience
    To prevent propagating errors downstream.
    Reactive
    For I/O heavy apps.
    Takeaways

    View Slide

  99. Beyond Future
    Because you were going to ask about coroutines…

    View Slide

  100. GraphQL Java
    There are already pure Kotlin libraries. Spring
    boot has a set of starters too.
    Coroutines
    Support in Spring. New utilities for Project
    reactor.
    Alternatives

    View Slide

  101. fun processQuery(query: String): Map
    @PostMapping(
    path = ["/graphql"],
    consumes = [MediaType.APPLICATION_JSON_VALUE],
    produces = [MediaType.APPLICATION_JSON_VALUE]
    )
    fun graphqlCoroutines(
    @RequestBody body: Map
    ): Mono> = mono(Unconfined) {
    val query: String = body["query"] as String? ?: "{}"
    graphQLService.processQuery(query)
    }
    REACTOR WITH COROUTINES

    View Slide

  102. Resources
    Write your own reactive and resilient GraphQL gateway in Kotlin

    View Slide

  103. 1
    2
    3
    Animal Facts GraphQL Gateway
    https://bitbucket.org/atlassianlabs/animal-facts-graphql-gateway-demo/src/
    master/
    graphql-java
    https://www.graphql-java.com/
    Resilience4j
    https://github.com/resilience4j/resilience4j
    Resources / Code

    View Slide

  104. 4 GraphiQL
    https://github.com/graphql/graphiql
    Resources / Code
    5 Coroutines-reactor
    https://github.com/Kotlin/kotlinx.coroutines/tree/master/reactive/kotlinx-coroutines-
    reactor

    View Slide

  105. 1
    2
    3
    Kotlin at Trello
    Trello tech blog:

    https://tech.trello.com/kotlin-at-trello/
    Engineering stateless, high-availability cloud services at Atlassian
    Atlassian Engineering Blog:
    https://www.atlassian.com/blog/technology/aws-scaling-multi-region-low-latency-
    service
    More about Micros
    Slightly outdated, but still relevant.

    https://www.youtube.com/watch?v=dc2nqzgqH24
    Resources / Further information

    View Slide

  106. 1 Why not both?
    https://knowyourmeme.com/memes/why-not-both-why-dont-we-have-both
    Resources / Other
    2 We are hiring!
    Jira Software is looking for engineers, Java and Kotlin
    http://go.atlassian.com/jsw-hiring

    View Slide

  107. MARTIN VARGA | SOFTWARE DEVELOPER | @MARTINTEEVARGA
    Future of Jira Software
    Powered by Kotlin

    View Slide

  108. View Slide