1.23.0 release notes

7th April 2023

🌸 Highlights

  • DocService now supports auto-completion for gRPC and Thrift services. #4516
    • Check out this awesome demo video by @Dogacel to learn more.

🌟 New features

  • You can now write a circuit breaker or retry rule that makes a decision based on gRPC trailers. #4496 #4535
final RetryRule rule =
  RetryRule.builder()
           .onGrpcTrailers((ctx, trailers) -> trailers.containsInt(
              "grpc-status", 13)) // Retry for internal error.
           .thenBackoff(backoff);
  • Unhandled exceptions are logged periodically out of the box. #4324 #4687

    ServerBuilder sb = ...
    // Log every 10 seconds.
    sb.unhandledExceptionsReportIntervalMillis(10000L);
  • You can now use Spring Boot 3 with Armeria. #4574

    • Use com.linecorp.armeria:armeria-spring-boot3-autoconfigure:1.23.0 dependency for the integration.
  • You can now disconnect connections gracefully using ClientRequestContext.initiateConnectionShutdown(). #4555 #4708

    ClientRequestContext ctx = ...
    // The connection will be gracefully shut down after a request is finished.
    ctx.initiateConnectionShutdown();
  • You can now use ServiceErrorHandler to configure the per-service or per-virtual host error handlers. #3421 #4716

    ServiceErrorHandler handler = (ctx, cause) -> {
      if (cause instanceof IllegalArgumentException) {
        return HttpResponse.of(HttpStatus.BAD_REQUEST);
      }
      // Return null to let the default error handler handle the exception.
      return null;
    };
    
    ServerBuilder sb = ...
    sb.route()
      .get("/foo")
      .errorHandler(handler)
      ...
      .build();
  • You can now handle an HTTP/1 request with an absolute URI using ServerBuilder.absoluteUriTransformer(). #4681 #4726

    ServerBuilder sb = ...
    sb.absoluteUriTransformer(absoluteUri -> {
      // https://foo.com/bar -> /bar
      return absoluteUri.replaceFirst("^https://\\.foo\\.com/", "/");
      // Or store the original URI in a query and use it in the proxy service.
      // return "/proxy?uri=" + URLEncoder.encode(absoluteUri);
    });
  • You can now listen to the result of DNS queries using DnsQueryListener. #4690 #4715

    DnsAddressEndpointGroup
      .builder("foo.com")
      .addDnsQueryListeners(new DnsQueryListener() {
        @Override
        void onSuccess(...) {...}
    
        @Override
        public void onFailure(...) {
          logger.warn("DNS query failed");
        }
      })
      .build();
  • You can now create a BlockingTaskExecutor by wrapping a ScheduledExecutorService. #4760

    • Additionally, you can create context-aware or context-propagating Executor and BlockingTaskExecutor.
    ScheduledExecutorService executorService = ...
    // Wrap a ScheduledExecutorService.
    BlockingTaskExecutor blockingTaskExecutor =
      BlockingTaskExecutor.of(executorService);
    
    RequestContext ctx = ...
    BlockingTaskExecutor blockingTaskExecutor = ...
    // context-aware
    ContextAwareBlockingTaskExecutor.of(context, executor);
    // context-propagating
    RequestContext.makeContextPropagating(blockingTaskExecutor);
  • You can now use StreamMessage.of() to convert an InputStream to StreamMessage. #3937 #4703

    InputStream in = ...
    // The input stream is divided into chunks of 8192 bytes by default.
    ByteStreamMessage stream = StreamMessage.of(in);
  • You can now use StreamMessage.streaming() to create StreamWriter for writing streaming messages. #4253 #4696

    StreamWriter<String> writer = StreamMessage.streaming();
    writer.write("foo");
    writer.write("bar");
    writer.close();
    
    // Subscribe to the writer.
    writer.subscribe(...);
  • You can now generate a RequestId using the information of RoutingContext. #4362 #4691

    • Additionally, the request ID generator can be configured per-service or per-virtual host. #4730 #4752
    ServerBuilder sb = ...
    sb.requestIdGenerator((routingCtx, req) -> {
      // Create a request ID from the trace ID of an OpenTelemetry headers.
      return RequestId.of(
        requestId(routingCtx.headers().get("traceparent")));
    });
  • You can now collect metrics of open and closed client connections using ConnectionPoolListener.metricCollecting(). #4685 #4686

    MeterRegistry registry = ...
    ConnectionPoolListener listener = ConnectionPoolListener.metricCollecting(registry);
    ClientFactory factory = ClientFactory.builder()
                                         .connectionPoolListener(listener)
                                         .build();
  • ClientRequestContext.authority() can now be used to get the authority that will be sent to the server. #4697

    ClientRequestContext ctx = ...
    String authority = ctx.authority();
  • You can now use ManagementServerProperties to configure the management server when using Spring integration. #4560 #4574

    armeria:
      ports:
        - port: 8080
    # Use a different port with a custom base path:
    management:
      server:
        port: 8443
        base-path: /foo
      endpoints:
        web:
          exposure:
            include: health, loggers, prometheus
  • You can now execute gRPC ServerInterceptor asynchronously in Kotlin using CoroutineServerInterceptor. #4669 #4724

    class AuthInterceptor : CoroutineServerInterceptor {
      private val authorizer = ...
    
      override suspend fun <ReqT, RespT> suspendedInterceptCall(
        call: ServerCall<ReqT, RespT>,
        headers: Metadata,
        next: ServerCallHandler<ReqT, RespT>
      ): ServerCall.Listener<ReqT> {
        val result = authorizer.authorize(ServiceRequestContext.current(), headers).await()
        if (result) {
          return next.startCall(call, headers)
        } else {
          throw AnticipatedException("Invalid access")
        }
      }
    }

📈 Improvements

  • The debug page of DocService is now a pop-up for easier access. #4599
  • You can now use Connection: close header to close a connection after sending or receiving a response. #4131 #4454 #4471 #4531
  • Armeria now exports the gauge metrics of CommonPools.workerGroup(). #4675 #4750
    • The metric names:
      • armeria.netty.common.event.loop.workers
      • armeria.netty.common.event.loop.pending.tasks
  • If the mapping for a HEAD request is not found, the request is rerouted to the service bound to the GET method on the same path. #4038 #4706

🛠️ Bug fixes

🏚️ Deprecations

☢️ Breaking changes

  • Spring Boot 1 integration is no longer supported. #4651 #4787

  • The type of blockingTaskExecutor property such as ServiceConfig.blockingTaskExecutor() is changed from ScheduledExecutorService to BlockingTaskExecutor. #4760

    • Simply recompiling your code should be enough in most cases because BlockingTaskExecutor is a ScheduledExecutorService.
  • The return types of makeContextAware methods on RequestContext are changed to the context-aware types. #4760

  • The signatures of UserClient.execute() have been changed. #4789

    • It now contains the newly added RequestTarget as a parameter instead of the path, query, and fragment.
  • The names of path cache meters have been changed. #4789

    • The old meter name: armeria.server.parsed.path.cache
    • New meter names:
      • armeria.path.cache{type=client}
      • armeria.path.cache{type=server}
  • Armeria client doesn't normalize consecutive slashes (e.g. foo//bar) in a client request path anymore. #4789

⛓ Dependencies

  • Brotli4j 1.9.0 → 1.11.0
  • Dropwizard 2.1.4 → 2.1.5
  • Dropwizard Metrics 4.2.15 → 4.2.18
  • fastutil 8.5.11 → 8.5.12
  • GraphQL Kotlin 6.3.5 → 6.4.0
  • java-jwt 4.2.2 → 4.3.0
  • Micrometer 1.10.3 → 1.10.5
  • Netty 4.1.87.Final → 4.1.91.Final
  • Reactor Kotlin 1.2.1 → 1.2.2
  • Sangria 3.5.0 → 3.5.3
  • Spring 5.3.25 → 6.0.6
  • Spring Boot
    • 3.0.5
    • 2.7.8 → 2.7.10

🙇 Thank you