Ratpack

Why ratpack?

  • Focused on testability

  • Short feedback loop

  • Starts up fast

  • Large user base at Target (and the twin cities)

    • #ratpack

What it is

  • Built on top of netty

  • Async from the ground up. 

  • Series of composable libraries called  modules .

 

What it isn't?

  • Not MVC
  • Not Full stack
  • Not convention over configuration
  • Not an implementation of the servlet specification. (Which means no servlet container like jetty)

HTTP framework choice is mostly personal preference.

Hello World

import ratpack.server.RatpackServer;

public class App {
    public static void main(String[] args) throws Exception {
        RatpackServer.start(serverSpec -> serverSpec
                .handlers(chain -> chain
                        .get(ctx -> ctx.render("Ole is pronounced Oh Lee"))
                )
        );
    }
}

Chain

The chain composes handlers together. It does no work other than delegating to the handlers based on the request. The all method is one way to build that chain.

Handlers

Each handler is a function which either responds to the request, or does some work and delegates to another handler. Our handler chain is a single function which is passed to the all method. This indicates that every request will be processed by this handler.

Promises

  • Access the HTTP Request and Response Delegation and flow control (via the next() and insert() methods) Access to contextual objects
  • Convenience for common handler operations
  • The context is a registry, you can think of it as a way to do DI with your handlers.

Context

  • Flatmap vs map
  • Blocking.get
  • Only subscribe once
RatpackServer.start(serverSpec -> serverSpec
    .handlers(chain -> chain
            .all(ctx -> {
                HttpClient httpClient = ctx.get(HttpClient.class);
                URI oUri = new URI(ctx.getRequest().getRawUri());

                URI proxyUri = new URI("http",
                        oUri.getUserInfo(),
                        HOST,
                        PORT,
                        oUri.getPath(),
                        oUri.getQuery(),
                        oUri.getFragment());

                ctx.getRequest().getBody().flatMap(incoming -> {
                            return httpClient.requestStream(proxyUri, requestSpec -> {
                                requestSpec.headers(mutableHeaders -> {
                                    mutableHeaders.copy(ctx.getRequest().getHeaders());
                                });
                                requestSpec.method(ctx.getRequest().getMethod());
                                requestSpec.body(b -> b.buffer(incoming.getBuffer()));
                            });
                        }).then(responseStream -> {
                            responseStream.forwardTo(ctx.getResponse());
                        });
            })
    )
);
class ReverseProxySpec extends Specification {
    @Shared
    ApplicationUnderTest aut = new MainClassApplicationUnderTest(App)

    TestHttpClient client = aut.httpClient

    @Shared
    EmbeddedApp proxiedHost = GroovyEmbeddedApp.of {
        handlers {
            all {
                render "rendered ${request.rawUri}"
            }
        }
    }

    def setupSpec() {
        System.setProperty('ratpack.proxyConfig.host', proxiedHost.address.host)
        System.setProperty('ratpack.proxyConfig.port', 
            Integer.toString(proxiedHost.address.port))
        System.setProperty('ratpack.proxyConfig.scheme', proxiedHost.address.scheme)
    }

    def "get request to ratpack is proxied to the embedded app"() {
        expect:
        client.getText(url) == "rendered /${url}"

        where:
        url << ["", "api", "about"]
    }
}

Integration Spec

Unit Spec

class CreateTaskV2HandlerSpec extends Specification {
    TaskService taskService = Mock()
    Validator validator = Mock(Validator)
    EventService eventService = Mock()
    LifecycleEventService lifecycleEventService = Mock()

    CreateTaskV2Handler createTaskV2Handler

    def setup() {
        createTaskV2Handler = new CreateTaskV2Handler(
            taskService, lifecycleEventService, eventService, validator)
    }

    def "returns 200 when task is created successfully"() {
        given:
        ObjectMapper objectMapper = new ObjectMapperBuilder().build()
        CreateTask createTask = new CreateTask()
        String body = objectMapper.writeValueAsString(createTask)

        TaskEntity expectedTask = new TaskEntity()
        List<RoutableEvent> expectedEvents = 
            [new RoutableEvent(new TaskStatusChanged(taskId: UUID.randomUUID()))]

        when:
        HandlingResult result = RequestFixture.handle(createTaskV2Handler) { fixture ->
            fixture.method("POST")
            fixture.body(body, "application/json").uri("/")
        }

        then:
        result.status.code == 200
        1 * validator.validate(_) >> []
        1 * taskService.createTask(_ as CreateTask) >> new TaskGeneratorResponse(
            [expectedTask] as Set, [] as Set)
        1 * lifecycleEventService.buildTaskCreatedLifecycleEvents(_ ) >> Promise.value(
            expectedEvents)
        1 * eventService.send(expectedTask.rootId, expectedEvents) >> Promise.value([])
        0 * _
    }
}

Flat Map vs Map

void handle(Context context) {
    context.parse(CreateTask).flatMap { CreateTask createRequest ->
        createTask(createRequest)
    }.flatMap { TaskGeneratorResponse taskGeneratorResponse ->
        publishLifecycleEvents(taskGeneratorResponse)
    }.mapError(TaskGenerationException) {
        throw new OleWMSException(400, it.message)
    }.mapError(DataAccessException) {
        handleDataAccessException(it)
    }.then { TaskStatusChanged taskStatusChanged ->
        context.render json(taskStatusChanged)
    }
}

Ole Best Practices

handlers {
    all(new MDCLoggingHandler())
    all(new MDCHeaderPropagationHandler())
    get('health/:name?', HealthCheckHandler)
    all(RequestLogger.ncsa())
    prefix('task_manager/v1') {
        insert(AdminChain)

        prefix('putawayaudit/tasks') {
            insert(PutawayAuditChain)
        }

        insert(TaskChain)

        insert(AssignChain)

        insert(ThreadChain)
    }

    prefix('task_manager/v2') {
        insert(TaskV2Chain)
    }
}

Chains

@Slf4j
class AdminChain extends GroovyChainAction {
    @Override
    void execute() throws Exception {
        get("admin/tasks/summary/pick", PickTaskSummaryHandler) // used by sango, should go away though
        get("admin/tasks/:barcode/by_barcode", TaskTreeByEntrypointHandler) // used by leatherman
        get("admin/tasks/:id/tree", GetTaskTreeHandler) // used by leatherman and sango
        get("admin/export/:root_id", ExportTaskByRootHandler)
        post("admin/tasks/:id/status", TaskUpdateStatusHandler) // used by an event processor
        put("admin/import/:root_id", ImportTaskByRootHandler)
        delete("admin/tasks/old", DeleteOldTasksHandler) // used by cleanup job, should just get rid of that one
        delete("admin/tasks/openBinAudit", DeleteBinAuditTasksHandler) //want to remove this
    }
}

Chain w/ By Method

@Override
    void execute() throws Exception {
        path('assign') {
            byMethod {
                post(AssignHandler)
                get(GetTaskTypesHandler)
            }
        }
    }

Handler Best Practices

  • One handler per path + verb
  • Integration Test for happy path scenario
  • Unit test for error scenarios
  • Keep HTTP logic in the handler, business logic in another layer

Adding Metrics

Instant start = Instant.now()
Blocking.get {
    eventPublisher.publish(events)
}.next {
    metricService.time('sendEvents', start)
}

Resources

  • Learning Ratpack
  • https://slack-signup.ratpack.io/
  • https://ratpack.io/
  • https://forum.ratpack.io/
  • http://ldaley.com/
  • http://kyleboon.org/
  • http://mrhaki.blogspot.com/
  • http://labs.adaptavist.com/code/2017/03/27/practical-ratpack-promises/
  • http://naleid.com/blog/2016/05/01/ratpack-executions-async-plus-serial-not-parallel/

deck

By Kyle Boon

deck

  • 33