Focused on testability
Short feedback loop
Starts up fast
Large user base at Target (and the twin cities)
#ratpack
Built on top of netty
Async from the ground up.
Series of composable libraries called modules .
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"))
)
);
}
}
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.
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.
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"]
}
}
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 * _
}
}
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)
}
}
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)
}
}
@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
}
}
@Override
void execute() throws Exception {
path('assign') {
byMethod {
post(AssignHandler)
get(GetTaskTypesHandler)
}
}
}
Instant start = Instant.now()
Blocking.get {
eventPublisher.publish(events)
}.next {
metricService.time('sendEvents', start)
}