0.98.0 release notes

12th February 2020

🎬 Before we begin

This release contains more breaking changes and deprecations than usual, which were necessary for the preparation of 1.0 release and the long term evolution of our API after 1.0. We'd like to apologize for any inconveniences caused by the breaking changes. Please don't forget we're always here for you and do let us know if you have any trouble upgrading, so we can help you!

🌟 New features

  • Since this release, in preparation of 1.0 release, we annotated classes and packages which may have a chance of breaking change with the @UnstableApi annotation. #2445
  • The attribute access API of RequestContext has been revamped for simplicity. #2322
    AttributeKey<String> MY_ATTR = AttributeKey.valueOf("MY_ATTR");
    RequestContext ctx = ...;
    ctx.setAttr(MY_ATTR, "foo");
    assert "foo".equals(ctx.attr(MY_ATTR));
  • When a RequestContext is derived or inherited from another RequestContext, the parent context's attributes are now visible from the derived or inherited context: #2322
    AttributeKey<String> MY_ATTR = AttributeKey.valueOf("MY_ATTR");
    ServiceRequestContext parentCtx = ...;
    ClientRequestContext childCtx = ...;
    assert childCtx.root() == parentCtx;
    parentCtx.setAttr(MY_ATTR, "foo");
    // Root context's attributes are visible from its children.
    assert "foo".equals(childCtx.attr(MY_ATTR));
    // Not visible anymore if the attribute is overwritten.
    childCtx.setAttr(MY_ATTR, "bar");
    assert "foo".equals(parentCtx.attr(MY_ATTR));
    assert "bar".equals(childCtx.attr(MY_ATTR));
  • {Client,Service}RequestContext now provides more useful ways to schedule request or response timeout: #2343
    Server.builder()
          .service("/svc", myService.decorate((delegate, ctx, req) -> {
              if (req.headers().contains("x-extend-timeout")) {
                  // Extend the timeout by 10 seconds.
                  ctx.extendRequestTimeout(Duration.ofSeconds(10));
              }
              return delegate.serve(ctx, req);
          }));
  • RequestLog API has been revamped for safety and usability. #2342
    RequestContext ctx = ...;
    // Asynchronous retrieval:
    ctx.log().whenRequestComplete().thenAccept(log -> {
        // Can't access response properties at compilation level.
        assert log instanceof RequestOnlyLog;
        System.err.println(log.toStringRequestOnly());
    })
    ctx.log().whenComplete().thenAccept(log -> {
        // Can access all properties.
        assert log instanceof RequestLog;
        System.err.println(log.toStringResponseOnly());
    });
    ctx.log().whenAvailable(RequestLogProperty.SESSION).thenAccept(log -> {
        // Advanced use case:
        // Raises an exception if accessing an unavailable property.
        System.err.println(log.channel());
    });
    // Accessing a property ensuring availability:
    ctx.log().ensureRequestComplete().requestEndTimeNanos();
    ctx.log().ensureComplete().responseEndTimeNanos();
    ctx.log().ensureAvailable(RequestLogProperty.RESPONSE_CONTENT)
       .responseContent();
  • RequestLog also has a new property NAME, which can be used as a method name in an RPC call, a span name in distributed tracing or any other human-readable string that can be used for identifying a request. #2413
  • Added a new immutable API for encoding and decoding HTTP query strings: #2307
    QueryParams params = QueryParams.builder()
                                    .add("foo", "1")
                                    .add("bar", "2")
                                    .build();
    // Encoding
    String queryString = params.toQueryString();
    assert "foo=1&bar=2".equals(queryString);
    // Decoding
    QueryParams decodedParams = QueryParams.fromQueryString("foo=1&bar=2");
    assert decodedParams.equals(params);
    // Mutation
    QueryParams newParams = params.toBuilder()
                                  .add("baz", "3")
                                  .build();
    assert "foo=1&bar=2&baz=3".equals(newParams.toQueryString());
  • Added various convenient boolean getter methods to HttpStatus: #2435
    assert HttpStatus.CONTINUE.isInformational();
    assert HttpStatus.OK.isSuccess();
    assert HttpStatus.FOUND.isRedirect();
    assert HttpStatus.BAD_REQUEST.isClientError();
    assert HttpStatus.SERVICE_UNAVAILABLE.isServerError();
    // No need to write like this anymore
    assert HttpStatus.OK.codeClass() == HttpStatusClass.SUCCESS;
  • Added MediaTypeNames which provides String version of well known MediaTypes, which is useful when writing an annotated service: #2438
    class MyAnnotatedService {
        @Get("/download/zip")
        @Produces(MediaTypeNames.ZIP)
        HttpResponse downloadArchive() { ... }
    }
  • You can now add {Request,Response}ConverterFunction and ExceptionHandlerFunction to all annotated services in your Server easily. #2316
    Server.builder()
          .annotatedService("/users", userService)
          .annotatedService("/posts", postService)
          .annotatedService("/files", fileService)
          // Applies all extensions to all 3 annotated services.
          .annotatedServiceExtensions(
              commonRequestConverters,
              commonResponseConverters,
              commonExceptionHandlers)
          .build();
  • You can now require a route to have HTTP headers and/or query parameters: #2102
    Server.builder()
          // Route to 'myService' only when:
          // - 'x-must-exist' header exists,
          // - and 'bar' query parameter exists.
          .route().get("/foo")
                  .matchesHeaders("x-must-exist")
                  .matchesParams("bar")
                  .build(myService)
  • You can now customize the SslContext created from ServerBuilder.tlsSelfSigned() or VirtualHost.tlsSelfSigned(): #2340
    Server.builder()
          .tlsSelfSigned()
          .tlsCustomizer(sslCtxBuilder -> {
              sslCtxBuilder.ciphers(...);
          })
  • You can now close an EndpointGroup asynchronously: #2430
    DnsAddressEndpointGroup group =
        DnsAddressEndpointGroup.of("cluster.com", 8080);
    group.whenClosed().thenRun(() -> {
        System.err.println("Closed!");
    });
    group.closeAsync();
  • You do not need to register your EndpointGroup to EndpointGroupRegistry for client-side load balancing. Just specify it when you build a client: #2381
    EndpointGroup group = EndpointGroup.of(
        Endpoint.of("node1.cluster.com"),
        Endpoint.of("node2.cluster.com"));
    // Thrift
    HelloService.Iface client =
        Clients.builder("tbinary+http", group)
               .path("/api/thrift/hello")
               .build(HelloService.Iface.class);
    // gRPC
    HelloServiceBlockingStub client =
        Clients.builder("gproto+http", group)
               .build(HelloServiceBlockingStub.class);
    // Web
    WebClient client =
        WebClient.of(SessionProtocol.HTTP, group);
  • You can now limit the number of endpoints in HealthCheckedEndpointGroup, which is very useful when there are many candidate endpoints in the group but you want to send requests to only a few of them, to avoid unnecessarily large number of outbound connections: #2177
    HealthCheckedEndpointGroup group =
        HealthCheckedEndpointGroup.builder(delegateGroup, "/health")
                                  .maxEndpointCount(3)
                                  .build();
  • You can now capture the ClientRequestContext of your client call with ClientRequestContextCaptor: #2344
    WebClient client = WebClient.of("http://foo.com/");
    try (ClientRequestContextCaptor ctxCaptor = Clients.newContextCaptor()) {
        HttpResponse res = client.get("/");
        ClientRequestContext ctx = ctxCaptor.get();
        ...
    }
  • Added ClientFactory.insecure() and ClientFactoryBuilder.tlsNoVerify() to simplify testing SSL/TLS connections with self-signed certificates: #2340
    // Using the default insecure factory
    WebClient.builder("https://127.0.0.1:8443")
             .factory(ClientFactory.insecure())
             .build();
    // Using a custom insecure factory
    WebClient.builder("https://127.0.0.1:8443")
             .factory(ClientFactory.builder()
                                   .tlsNoVerify()
                                   ...
                                   .build())
             .build();
  • ClientFactory is now part of ClientOptions for easier creation of derived clients. #2384
    ClientFactory factory = ...;
    WebClient client =
        WebClient.builder(...)
                 .factory(factory)
                 .build();
    WebClient clientCopy =
        WebClient.builder(...)
                 .options(client.options())
                 .build();
    // Note that ClientFactory is copied as well.
    assert client.factory() == clientCopy.factory();
  • RetrofitMeterIdPrefixFunction is now capable of adding HTTP method and request path pattern if you specify a Retrofit service class: #2356
  • New module armeria-dropwizard provides the integration with Dropwizard, which allows you to leverage the best of the both worlds. #2236
  • You can now customize DocService when integrating with Spring framework by injecting DocServiceConfigurator: #2327
    @Bean
    public DocServiceConfigurator docServiceConfigurator() {
        // Exclude all Thrift services from DocService.
        return docServiceBuilder -> {
            docServiceBuilder.exclude(DocServiceFilter.ofThrift());
        };
    }
  • ServerRule (JUnit 4) and ServerExtension (JUnit 5) now have more getters: #2449
    • Endpoint getters:
      • endpoint(SessionProtocol), httpEndpoint() and httpsEndpoint()
    • URI getters:
      • uri(SessionProtocol), uri(SessionProtocol, SerializationFormat), httpUri(), httpUri(SerializationFormat), httpsUri() and httpsUri(SerializationFormat)
      • The old deprecated getters return String instead of URI.
    • InetSocketAddress getters:
      • socketAddress(SessionProtocol)
  • The CompletableFutures returned by our API will leave a warning log like the following when you perform a blocking operation in an event loop thread: #2275
    Calling a blocking method on CompletableFuture from an event loop or
    non-blocking thread. You should never do this ...
    java.lang.IllegalStateException: Blocking event loop, don't do this.
    You can disable this functionality by specifying the -Dcom.linecorp.armeria.reportBlockedEventLoop=false JVM option.
  • You can now serialize and deserialize ThriftCall, ThriftReply, TMessage and TBase into TTEXT JSON using ThriftJacksonModule. #2439
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new ThriftJacksonModule());
  • You can now make any SLF4J Logger context-aware with RequestContext.makeContextAware(Logger): #2341
    // Prints 'Hello!'
    Logger logger = ...;
    logger.info("Hello!");
    // Prints '[<current context>] Hello!'
    Logger ctxAwareLogger = ctx.makeContextAware(logger);
    ctxAwareLogger("Hello!");
  • RequestContextExporter is now part of the core API, allowing you to integrate with other logging frameworks than Logback, such as Log4J2. #2314
  • You can now disable HTTP header validation the the -Dcom.linecorp.armeria.validateHeaders=false JVM option.

📈 Improvements

  • Slightly reduced memory footprint of Logging{Client,Service} #2341
  • UnknownHostException raised by Armeria now explains what DNS query has failed. #2332
  • WebClient now accepts a URI that starts with none+ as well. #2361
  • HealthCheckedEndpointGroup now logs a helpful warning message when it receives a 4XX response. #2401
  • Our builder API Javadoc does not show mysterious return type parameter such as B and SELF anymore. #2454
  • A client now gets SSLException instead of ClosedSessionException if a connection attempt fails with an SSL/TLS handshake error. #2338

🛠️ Bug fixes

  • WebClient does not omit a request query string when sending a request to an absolute URI. #2309
  • A user cannot start a Server with a misconfigured SslContext anymore. #2325
  • A user now always gets the correct RequestContext even if the contexts are nested. #1083
  • Fixed a bug where thread-local context customizers were called for derived contexts unintentionally. #2344
  • Clients.withHttpHeaders() and withContextCustomizer() now work with gRPC calls. #2344
  • ClientRequestContext.path() now returns a correct path for gRPC client calls. #2344
  • You can now send a POST request with an empty body with DocService client. #2357
  • Server-side route cache hit ratio was not as good as we intended. #2358
  • Fixed various potential memory leak in HttpResponseWriter. #2359
  • Long-polling health check mechanism now detects a stall server which accepts a connection but does not send any response. #2392
  • ClientFactoryOptions does not raise a NullPointerException anymore. #2387
  • An AsyncMethodCallback specified when making an async Thrift call now has thread-local ClientRequestContext set properly. #2383
  • gRPC client and server now works well with non-official gRPC stub generators such as reactive-grpc. #2376
  • Fixed a bug where a Server can be started back after close() is called. #2406
  • Fixed a regression where Reactor does not treat Armeria's event loop threads as non-blocking. #2404
  • Armeria does not fail to initialize anymore even if it failed to load the com.linecorp.armeria.versions.properties file. #2398
  • You'll not see the cannot start a new stream with a DATA frame errors anymore. #2429
  • RequestLog.requestStartTime property now includes the time taken for making a connection attempt and the time taken by decorators. #2436
  • The -Dcom.linecorp.armeria.dumpOpenSslInfo=true JVM option does not trigger a StackOverflowError anymore. #2418
  • Fixed cosmetic issues in DocService client sidebar. #2470
  • Made sure IPv6 DNS queries are not sent to some IPv4 only machines with a link-local IPv6 interface. #2464

🏚️ Deprecations

  • HttpParameters has been deprecated in favor of QueryParams #2307
  • ServerBuilder.tls() methods that require a Consumer have been deprecated. Use tlsCustomizer(Consumer) instead. #2340
    // Before:
    Server.builder()
          .tls(..., sslCtxBuilder -> { ... });
    // After:
    Server.builder()
          .tls(...)
          .tlsCustomizer(sslCtxBuilder -> { ... });
  • Many classes which have Http in their names have been deprecated in favor of those without Http, e.g. #2323
    • RetryingHttpClient -> RetryingClient
    • HttpFileService -> FileService
  • Many builder classes' constructors have been deprecated in favor of builder() static methods, e.g. #1719
    • new ClientBuilder() -> Clients.builder()
    • new ArmeriaRetrofitBuilder() -> ArmeriaRetrofit.builder()
    • new ServerCacheControlBuilder() -> ServerCacheControl.builder()
  • Many public static final fields that are not truly constants have been deprecated in favor of static factory methods, e.g.
    • EndpointSelectionStrategy.ROUND_ROBIN -> roundRogin()
    • NodeValueCodec.DEFAULT -> ofDefault()
    • AuthTokenExtractors.BASIC -> basic()
  • MoreNamingConventions has been deprecated because we follow Micrometer's recommended naming convention. #2367
  • Version.identify() has been deprecated in favor of getAll() #2398
  • ServiceRequestContext.setRequestTimeout*() has been deprecated in favor of extendRequestTimeout*(), setRequestTimeoutAfter*(), setRequestTimeoutAt*() and clearRequestTimeout(). #2343
  • ClientRequestContext.setResponseTimeout*() has been deprecated in favor of extendResponseTimeout*(), setResponseTimeoutAfter*(), setResponseTimeoutAt*() and clearResponseTimeout(). #2343
  • Many methods that return a CompletableFuture have been renamed from *Future() to when*(), e.g. #2427
    • HttpRequest.completionFuture() -> whenComplete()
    • HttpResponseWriter.onDemand() -> whenConsumed()
    • EndpointGroup.initialEndpointsFuture() -> whenReady()
  • Many URI-returning methods in ServerRule and ServerExtension have been deprecated in favor of the new methods that do not require a path parameter: #2449
    ServerExtension server = ...;
    // Before
    server.httpUri("/");
    server.httpUri("/foo");
    // After
    server.httpUri();
    server.httpUri().resolve("/foo");
  • THttpService.allowedSerializationFormats() has been deprecated in favor of supportedSerializationFormats() for consistency with GrpcService. #2453
  • Service.decorate(Class) has been deprecated in favor of other decorate() methods that require a function.
  • ClosedPublisherException has been deprecated in favor of ClosedStreamException. #2468

☢️ Breaking changes

  • Most revamped APIs in this release were changed in a backward-incompatible way:
    • RequestLog
    • Attribute API in RequestContext
  • Content previewing mechanism has been revamped into decorators. Use ContentPreviewingClient and ContentPreviewingService.
  • You cannot add callbacks to RequestContext anymore because we found this feature results in poor performance and confusing behavior in many cases. We may want to revisit this feature if there is a valid use case for it.
  • {Server,VirtualHost}Builder.tls() methods do not throw an SSLException anymore. build() will throw an IllegalStateException instead. As a result, any SSL configuration failure will be known when a user calls build(), rather than tls().
  • We were not able to keep some classes or method signatures while we remove Http from class names.
  • ServiceRequestContext.logger() has been removed due to performance issues with Log4J2.
  • RequestContext.isTimedOut() has been removed.
  • We do not support Tomcat 8.0 anymore, which was obsoleted by Tomcat 8.5 anyway.
  • The classes in armeria-grpc-protocol have been reorganized into multiple packages.
  • Our Micrometer meter IDs have been changed, which means you might need to update your monitoring configuration. If you wish to switch back to the legacy naming style, specify the -Dcom.linecorp.armeria.useLegacyMeterNamed=true JVM option. However, please keep in mind this option will eventually go away, because the new naming convention is recommended by Micrometer.
  • All our methods do not return Optional anymore. They are all @Nullable now. If you wish to continue using Optional, just wrap the return value with Optional.ofNullable().
  • EndpointGroupRegistry has been removed, because you can now just specify an EndpointGroup directly when creating a client.
    • As a result, you need to specify an EndpointSelectionStrategy when building an EndpointGroup. If unspecified, EndpointSelectionStrategy.weightedRoundRobin() is used.
  • MeterIdPrefixFunction is not a functional interface anymore. You must implement two methods explicitly: activeRequestPrefix() and completeRequestPrefix().
  • Now that ClientFactory is a part of ClientOption, the following code will not work as expected, because options(options) will overwrite the factory.
    ClientOptions options = ClientOptions.builder().maxResponseLength(...).build();
    ClientFactory factory = ClientFactory.builder().useHttp2Preface(false).build();
    WebClient client = WebClient.builder(...)
                                .factory(factory)
                                .options(options)
                                .build();
    // This will fail!
    assert client.options().factory() == factory;
    To fix this, you must call options() first, and then override the individual properties:
    WebClient client = WebClient.builder(...)
                                .options(options)
                                .factory(factory)
                                .build();
  • StreamMessage.subscribe(..., boolean withPooledObjects) has been removed. Use subscribe(..., SubscriptionOption.WITH_POOLED_OBJECTS).
  • StreamMessage.drainAll(..., boolean withPooledObjects) has been removed. Use drainAll(..., SubscriptionOption.WITH_POOLED_OBJECTS).
  • HttpRequestDuplicator and HttpResponseDuplicator are now interfaces. Use HttpRequest.toDuplicator() and HttpResponse.toDuplicator() to create a duplicator.
  • StructuredLog and StructuredLoggingService have been removed. Use AccessLogWriter.
  • ThriftStructuredLogJsonFormat has been removed. Register ThriftJacksonModule to ObjectMapper to serialize or deserialize Thrift objects.

⛓ Dependencies

  • Brave 5.9.1 -> 5.9.3
  • Dropwizard 1.3.17 -> 1.3.18
  • Dropwizard Metrics 4.1.1 -> 4.1.2
  • gRPC 1.25 -> 1.27
  • Jackson 2.10.1 -> 2.10.2.20200130
  • java-jwt 3.8.3 -> 3.9.0
  • Jetty 9.4.24 -> 9.4.26
  • Micrometer 1.3.2 -> 1.3.3
  • Netty 4.1.43 -> 4.1.45
    • TCNative BoringSSL 2.0.26 -> 2.0.28
  • Prometheus Java client 0.8.0 -> 0.8.1
  • Reactor 3.3.1 -> 3.3.2
  • Retrofit 2.6.2 -> 2.7.1
  • RxJava 2.2.15 -> 2.2.17
  • SLF4J 1.7.29 -> 1.7.30
  • Spring Boot 2.2.1 -> 2.2.4, 2.1.10 -> 2.1.12
  • Thrift 0.12.0 -> 0.13.0
  • Tomcat 9.0.29 -> 9.0.30, 8.5.49 -> 8.5.50

🙇 Thank you