1.15.0 release notes

24th March 2022

🌸 Highlights

  • gRPC health check protocol
  • Multipart file uploads in annotated services
  • Preliminary GraphQL support in DocService
  • Refactored DNS resolver cache with higher hit ratio and reduced traffic
  • New metrics that help you get alerts before your TLS certificates are expired

🌟 New features

  • Server now registers two gauges about TLS certificate expiry, so you can get alerts before the certificates are expired. #4075 #4082

    • armeria_server_certificate_validity
      • 1 if certificate is in validity period. 0 otherwise.
    • armeria_server_certificate_validity_days
      • The number of days before certificate expires, or -1 if expired.
  • You can now specify a File or Path parameter in your annotated service method to handle multipart file uploads. #3578 #4061

    Server
      .builder()
      .annotatedService(new Object() {
        @Post("/upload")
        @Consumes("multipart/form-data")
        public String upload(@Param File file) {
          return "Successfully uploaded a file to " + file;
        }
      })
      .build();
  • You can now use the || (logical OR) operator in parameter and header predicate expressions. #4116 #4138

    // "Hi!" for /greet?casual or /greet?casual=true
    // "Hello!" for /greet or /greet?casual=false
    Server
      .builder()
      .route()
        .get("/greet")
        .matchesParam("casual || casual=true") // 👈👈👈
        .build((ctx, req) -> HttpResponse.of("Hi!"))
      .route()
        .get("/greet")
        .matchesParam("!casual || casual!=true") // 👈👈👈
        .build(ctx, req) -> HttpResponse.of("Hello!"))
  • You can now enable gRPC health check protocol with GrpcHealthCheckService #3146 #3963

    Server
      .builder()
      .service(
        GrpcService
          .builder()
          .addService(new MyGrpcService())
          .enableHealthCheckService(true) // 👈👈👈
          // or customize:
          // .addService(GrpcHealthCheckService.of(...))
          .build()
      )
      .build()
  • DocService now supports GraphqlService with CodeMirror auto-completion. #3706 #4023

  • You can now configure the EndpointGroup implementations based on DynamicEndpointGroup to allow or disallow empty endpoints. This can be useful when you want to avoid the situation where the endpoint group becomes empty due to misconfiguration. #3952 #3958

    EndpointGroup endpointGroup =
      DnsAddressEndpointGroup
        .builder("example.com")
        .allowEmptyEndpoints(false) // 👈👈👈
        .build();
  • You can now configure the timeout of DNS sub-queries with queryTimeoutMillisForEachAttempt(). Previously, you were only able to specify the timeout for the entire DNS resolution process, which can consist of more than one DNS query. #2940 #4133

    DnsAddressEndpointGroup
      .builder("armeria.dev")
      .queryTimeoutMillisForEachAttempt(1000) // 👈👈👈
      .queryTimeoutMillis(5000)
      .build();
  • Armeria now has the global shared DNS cache by default for higher hit ratio and less DNS traffic. You can also build and share a custom DNS cache across multiple resolvers. #2940 #4133

    DnsCache dnsCache =
      DnsCache
        .builder()
        .ttl(60, 3600)
        .negativeTtl(600)
        .build();
    
    EndpointGroup endpointGroup =
      DnsAddressEndpointGroup
        .builder("armeria.dev")
        .dnsCache(dnsCache) // 👈👈👈
        .build();
    
    ClientFactory factory =
      ClientFactory
        .builder()
        .domainNameResolverCustomizer(builder -> {
          builder.dnsCache(dnsCache); // 👈👈👈
        })
        .build();
    
    WebClient client =
      WebClient
        .builder(SessionProtocol.HTTPS, endpointGroup) // 👈👈👈
        .factory(factory) // 👈👈👈
        .build();
  • Failed search domain queries are now cached according to the negative TTL configuration. #2940 #4133

  • You can now specify a SuccessFunction when constructing a client or server, and let all decorators use it for determining whether a request was successful or not. #4101

    Server
      .builder()
      .successFunction((ctx, log) -> { // 👈👈👈
        // Treat only '200 OK' and '204 No Content' as success.
        switch (log.responseStatus().code()) {
          case 200:
          case 204:
            return true;
          default:
            return false;
        }
      })
      .decorator(LoggingService.newDecorator())
      .decorator(MetricCollectingService.newDecorator())
      ...
    
    WebClient
      .builder()
      .successFunction(...) // 👈👈👈
      .decorator(LoggingClient.newDecorator())
      .decorator(MetricCollectingClient.newDecorator())
      .build();
  • You can now specify different sampling ratio (or Sampler) for successful and failed requests when using LoggingClient and LoggingService #3666 #4101

    LoggingService
      .builder()
      // Log at 10% sampling ratio if successful.
      .successSamplingRate(0.1)
      // Log all failure.
      .failureSamplingRate(1.0)
      .newDecorator()
  • You can now customize how LoggingClient and LoggingService determines log level more flexibly. #3972

    LoggingService
      .builder()
      // Log at INFO if under /important. DEBUG otherwise.
      .requestLogLevelMapper(log -> {
        if (log.requestHeaders().path().startsWith("/important/")) {
          return LogLevel.INFO;
        } else {
          return LogLevel.DEBUG;
        }
      })
      // Log at INFO if 200 OK.
      .responseLogLevel(HttpStatus.OK, LogLevel.INFO)
      // Log at WARN if 5xx.
      .responseLogLevel(HttpStatusClass.SERVER_ERROR, LogLevel.WARN)
      .newDecorator()
  • You can now specify a customizer that customizes a ClientRequestContext when building a client. #4075 #4082

    WebClient client =
      WebClient
        .builder()
        .contextCustomizer(ctx -> { // 👈👈👈
          String userId = MyUserContext.current().getId();
          ctx.setAttr(USER_ID, userId);
        })
        .build();

    For instance, you could manually propagate a Brave thread-local TraceContext using it:

    Tracing reactorTracing = ...;
    Tracing requestContextTracing =
      Tracing
        .newBuilder()
        .currentTraceContext(RequestContextCurrentTraceContext.ofDefault())
        .build();
    
    WebClient
      .builder()
      .contextCustomizer(TraceContextPropagation.inject(() -> {
        // Propagate Reactor's TraceContext to Armeria's TraceContext.
        return reactorTracing.currentTraceContext().get();
      })
      .decorator(BraveClient.newDecorator(requestContextTracing))
      .build();
  • You can now write the content of StreamMessage into a file using StreamMessage.writeTo() #4048 #4130

    SplitHttpResponse res =
      WebClient
        .of()
        .get("https://example.com/large_file")
        .split();
    StreamMessage<HttpData> body = res.body();
    body.writeTo( // 👈👈👈
      Function.identity(),
      Path.of("/tmp", "large_file")
    );
  • You can now convert a StreamMessage into an InputStream using StreamMessage.toInputStream() #4059

    StreamMessage<HttpData> body = ...;
    BufferedReader in = new BufferedReader(
      new InputStreamReader(
        body.toInputStream(Function.identity()), // 👈👈👈
        "UTF-8"
      )
    );
    for (;;) {
      String line = in.readLine();
      if (line == null) break;
      System.out.println(line);
    }
  • You can now use StreamMessage.mapParallel() to modify a stream using an async operation with a configurable concurrency limit. #4031

  • You can now decode a StreamMessage into another using StreamMessage.decode() which was previously possible only for HttpMessage. #4147 #4148 #4152

  • You can now give Armeria a performance optimization hint by specifying if your service is unary, request-streaming, response-streaming or bidi-streaming by implementing HttpService.exchangeType() #3956

    class MyUnaryService implements HttpService {
      @Override
      public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) {
        return HttpResponse.from(
          req
            .aggregate() // Always aggregate
            .thenApply(aggregatedReq -> {
              HttpResponse.of( // Never streaming
                "You sent %d bytes.",
                aggregatedReq.content().length()
              );
            })
        );
      }
    
      // Tells Armeria to assume non-streaming requests and responses.
      @Override
      public ExchangeType exchangeType() {
        return ExchangeType.UNARY; // 👈👈👈
      }
    }

📈 Improvements

  • Spring Boot WebServerException message is now more user-friendly. #4146

🛠️ Bug fixes

🏚️ Deprecations

  • samplingRate property of @LoggingDecorator has been deprecated in favor of successSamplingRate and failureSamplingRate.
  • The successFunction() builder methods that require a BiPredicate has been deprecated in favor of SuccessFunction that's specified when constructing a client or server.
  • dnsServerAddressStreamProvider() has been deprecated in favor of serverAddressStreamProvider() in the DNS-related builders.
  • disableDnsQueryMetrics() has been deprecated in favor of enableDnsQueryMetrics() in the DNS-related builders.

☢️ Breaking changes

⛓ Dependencies

  • Bucket4j 7.0.0 → 7.3.0
  • Curator 5.2.0 → 5.2.1
  • Dropwizard Metrics 4.2.7 → 4.2.9
  • gRPC-Java 1.43.2 → 1.45.0
  • Jackson 2.13.1 → 2.13.2
  • java-jwt 3.18.3 → 3.19.0
  • Jetty 9.4.44 → 9.4.45
  • Graphql-Java 17.3 → 18.0
  • Logback 1.2.10 → 1.2.11
  • Micrometer 1.8.2 → 1.8.4
  • Netty 4.1.73 → 4.1.75
    • io_uring 0.0.11 → 0.0.13
  • Prometheus 0.14.1 → 0.15.0
  • Reactor 3.4.14 → 3.4.16
    • Reactor Kotlin 1.1.5 → 1.1.6
  • RxJava 3.1.3 → 3.1.4
  • Sangria 2.1.6 → 3.0.0
  • ScalaPB 0.11.8 → 0.11.10
  • scala-collection-compat 2.6.0 → 2.7.0
  • SLF4J 1.7.34 → 1.7.36
  • Spring 5.3.15 → 5.3.17
  • Spring Boot 2.6.3 → 2.6.5

🙇 Thank you