Your go-to microservice framework for any situation, from the creator of Netty et al. You can build any type of microservice leveraging your favorite technologies, including gRPC, Thrift, Kotlin, Retrofit, Reactive Streams, Spring Boot and Dropwizard.
APACHE-2.0 License
Bot releases are visible (Hide)
RequestContext.newDerivedContext(Request)
cookie
header handlingNoSuchElementException
in ZooKeeperUpdatingListener
Netty 4.1.19.Final has a data corruption issue when epoll transport is used. Specify the following JVM option to disable epoll transport and work around this problem until Netty 4.1.20.Final is released:
-Dcom.linecorp.armeria.useEpoll=false
@RequestConverter
and @RequestObject
annotation which enable request conversion for an annotated serviceRequestContext.newDerivedContext()
method which creates a new context with the same properties (except the request log) from an existing contextRequestLog
can now contain one or more child RequestLog
s, which is useful for recording the retry history of a request. From this release, RetryingRpcClient
and RetryingHttpClient
leverage this feature.HttpHeaders.contentType()
which accesses the content-type
header using MediaType
HttpRequest
and HttpResponse
use highly-optimized dedicated Reactive Stream implementations for better performance and reduced memory footprint.@Param
annotation.RetryingHttpClient
does not create a new context for each retry attempt.HttpVfs
implementation exposed by DocService
generated poor meter tag.CompletionStage
handling in annotated services#909 The void doXXX()
methods in AbstractHttpService
have been deprecated in favor of the HttpResponse doXXX()
methods: (Note the return type.)
// Before
public class MySerivce extends AbstractHttpService {
@Override
public void doGet(ServiceRequestContext ctx, HttpRequest req, HttpResponseWriter res) {
res.respond(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF8, "Hello, World!");
}
}
// After
public class MySerivce extends AbstractHttpService {
@Override
public HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) {
return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF8, "Hello, World!");
}
}
This change gives you the freedom of choosing better HttpResponse
implementation depending on his or her needs. See #846 and #889 for more information.
#909 DefaultHttpRequest
and DefaultHttpResponse
have been deprecated in favor of HttpRequest.streaming()
and HttpResponse.streaming()
. See the pull request for the detailed migration instructions.
#909 The HttpResponseWriter.respond()
methods were all deprecated. Create a new HttpResponse
using the factory methods of HttpResponse
instead.
HttpVfs
is not a functional interface anymore, due to its new method meterTag()
which is used as a tag value of the cache metrics.-Dcom.linecorp.armeria.defaultUseHttp1Pipelining=true
JVM option.@Converter
annotation has been replaced by @ResponseConverter
for consistency with other annotations such as @RequestConverter
and @ExceptionHandler
.ExceptionHandlerFunction
interface has been revamped for more flexibility.HttpRequest.isKeepAlive()
has been removed because it had no meaning and effect for most cases.Netty 4.1.19.Final has a data corruption issue when epoll transport is used. Specify the following JVM option to disable epoll transport and work around this problem until Netty 4.1.20.Final is released:
-Dcom.linecorp.armeria.useEpoll=false
FilteredStreamMessage
and FilteredHttpResponse
Subscriber
of PublisherBasedStreamMessage
is sometimes not notifiedAbortedStreamException
Note: Use Armeria 0.55.1.
EndpointSelector.select()
has been changed.StreamMessage.closeFuture()
has been renamed to completionFuture()
.StreamMessage.subscribe(..., Executor, ...)
has been replaced with subscribe(..., EventExecutor, ...)
.
EventLoop
or an EventExecutor
instead of a plain Executor
.StreamMessage
now always invokes its Subscriber
from an EventExecutor
(or an EventLoop
), even if a user did not specify any EventExecutor
with subscribe()
. Previously, the Subscriber
, which subscribed without an Executor
, was invoked from the same thread with the publisher, but this is not always true anymore.
EventLoop
threads due to the timing issues introduced by this change.#804 StickyEndpointSelectionStrategy
ToLongFunction<ClientRequestContext> hasher = (ClientRequestContext ctx) -> {
return ((HttpRequest) ctx.request()).headers().get(AsciiString.of("cookie")).hashCode();
};
EndpointGroupRegistry.register(
"myGroup", new StaticEndpointGroup(Endpoint.of("foo.com", 80), Endpoint.of("bar.com", 80)),
new StickyEndpointSelectionStrategy(hasher));
#807 Clients.withContextCustomizer()
which allows a user change the properties of a ClientRequestContext
without using a decorator:
try (SafeCloseable ignored = Clients.withContextCustomizer(ctx -> {
ctx.attr(USER_ID).set(userId);
ctx.attr(USER_SECRET).set(secret);
})) {
client.executeSomething(..);
}
#813 HttpHeaderNames.LINK
has been added.
#824 @Decorator
annotation which decorates an annotated service:
public class MyAnnotatedService {
@Get("/my_service")
@Decorator(MyFirstDecorator.class)
@Decorator(MySecondDecorator.class)
public String myService(ServiceRequestContext ctx, HttpRequest req) { ... }
}
#825 @Header
annotation which injects an HTTP header value for an annotated service:
public class MyAnnotatedService {
@Get("/some_resource")
public String aHeader(@Header("if-match") String ifMatch) { ... }
}
#829 #837 Additional configuration properties for HTTP/1-related limits:
final ClientFactory cf = new ClientFactoryBuilder()
.maxHttp1InitialLineLength(8192)
.maxHttp1HeaderSize(16384)
.maxHttp1ChunkSize(16384)
.build();
final ServerBuilder sb = new ServerBuilder()
.maxHttp1InitialLineLength(8192)
.maxHttp1HeaderSize(16384)
.maxHttp1ChunkSize(16384);
#835 Allow specifying remote service name for a Zipkin span when using HttpTracingClient
.
#847 @ExceptionHandler
annotation which allows a user handle an exception conveniently:
@ExceptionHandler(MyExceptionHandlerFunction.class)
public class MyAnnotatedService {
@Get("/users/{id}")
...
}
public class MyExceptionHandlerFunction implements ExceptionHandlerFunction {
@Override
public HttpResponse handle(RequestContext ctx, HttpRequest req, Throwable cause) { ... }
}
#855 EventLoopStreamMessage
which is an alternative experimental StreamMessage
implementation faster than DefaultStreamMessage
in certain cases.
GrpcService
DefaultStreamMessage
null
in an annotated service method.RequestLogBuilder.endResponse()
will use RpcResponse.cause()
as the response failure cause automatically if possible.KeeperException.NoNodeException
in ZooKeeperConnector
due to a race conditionStreamMessage
implementationsIndexOutOfBoundsException
while recording request metrics
CKMSQuantiles
with RollingHdrQuantiles
based on HrdHistogram
armeria.hdrHistogram.count
- the number of histogram objects in use by Armeriaarmeria.hdrHistogram.estimatedFootprint
- the total estimated memory footprint of all histogram objects in use by Armeria, in bytes.com.linecorp.armeria.server.annotation.Optional
with com.linecorp.armeria.server.annotation.Default
.#18 #726 Added ThrottlingHttpService
and ThrottlingRpcService
, which reject incoming requests based on ThrottlingStrategy
ServerBuilder sb = new ServerBuilder();
sb.service("/foo", myService.decorate(
ThrottlingHttpService.newDecorator(
new RateLimitingThrottlingStrategy<>(10.0 /* requests per second */))));
#678 #772 Added LoggingClientBuilder
for more customization of LoggingClient
HttpClient client = new HttpClientBuilder("http://example.com/")
.decorator(new LoggingClientBuilder()
.requestLogLevel(LogLevel.INFO)
.successfulResponseLogLevel(LogLevel.INFO)
.failureResponseLogLevel(LogLevel.WARN)
.newDecorator())
.build();
#689 Replaced HttpVfs entry cache to Caffeine cache
armeria.server.file.vfsCache
.#738 #775 Support Optional<T>
parameter type for annotated services.
public class MyAnnotatedService {
@Post("/style_1")
public AggregatedHttpMessage postWithOptional(@Param("type") Optional<String> type,
@Param("value") String value) {
final String actualType = type.orElse("plaintext");
...
}
@Post("/style_2")
public AggregatedHttpMessage postWithDefault(@Param("type") @Default("plaintext") String type,
@Param("value") String value) {
...
}
}
#785 Added ConcurrentCompositeMeterRegistry
which works around the known scalability issue with Micrometer CompositeMeterRegistry
. We will remove this class once we upgrade to Micrometer 1.0.0-rc.3 or above.
400 Bad Request
rather than 500 Internal Server Error
when a handler method throws an IllegalArgumentException
.NoSuchElementException
when no MeterRegistry
bean is present.RetryingRpcClient
.NullPointerException
is raised when one-way Thrift method fails with an exception.MeterIdFunction
does not add the hostnamePattern
tag.This release contains the following commits backported from Armeria 0.53.0. Please note that it's strongly recommended to upgrade to 0.53.0, however.
#696 Replace PathMappings
with Router
and change the API of PathMapping
#717 #727 #729 #733 #737 #747 Use Micrometer as the primary metric collection library.
Replace (Dropwizard|Prometheus)MetricCollecting(Client|Service)
with
MetricCollecting(Client|Service)
ServerBuilder sb = new ServerBuilder();
sb.service(...., myService.decorate(
MetricCollectingService.newDecorator(MeterIdFunction.ofDefault())));
Replace DropwizardMetricsCircuitBreakerListener
with
MetricCollectingCircuitBreakerListener
Replace PathMapping.metricName()
with PathMapping.meterTag()
Add Server.meterRegistry()
and ClientFactory.meterRegistry()
The default registry is Metrics.globalRegistry
.
A user can override the default registry using ServerBuilder.meterRegistry(..)
, ClientFactoryBuilder.meterRegistry(..)
and ClientFactory.setMeterRegistry(..)
.
ServerBuilder sb = new ServerBuilder();
sb.meterRegistry(NoopMeterRegistry.get()); // Disable metrics
Add RequestContext.meterRegistry()
Add various utilities for Micrometer to common.metric
#720 RetryStrategy.shouldRetry()
now returns CompletableFuture<Optional<BackOff>>
to allow a user to choose a different Backoff
for a different failure cause.
// Use fixed delay on 500 and exponential delay on 503.
Backoff backoffOn500 = Backoff.fixed(3000);
Backoff backoffOn503 = Backoff.exponential(1000, 60000);
RetryStrategy<HttpRequest, HttpResponse> strategy = RetryStrategy.onStatus(httpStatus -> {
if (httpStatus == HttpStatus.SERVICE_UNAVAILABLE) {
return Optional.of(backoffOn503);
} else if (httpStatus == HttpStatus.INTERNAL_SERVER_ERROR) {
return Optional.of(backoffOn500);
} else {
return Optional.empty();
}
});
#749 Rename PrometheusExporterHttpService
to PrometheusExpositionService
#690 #710 Easier Server
integration test with ServerRule
A new module armeria-testing
contains a JUnit rule ServerRule
, which enables easier integration testing of a Server
.
public class MyTest {
@ClassRule // or @Rule if you want to start a new Server for each test method.
public static final ServerRule server = new ServerRule() {
@Override
protected void configure(ServerBuilder sb) throws Exception {
sb.service("/myService", ...);
}
});
@Test
public void myTest() throws Exception {
// ServerRule provides various utility methods for getting URIs and port numbers.
HttpClient client = HttpClient.of(server.uri("/"));
AggregatedHttpMessage response = client.get("/myService").aggregate().get();
assertThat(response.content().toStringUtf8()).isEqualTo("Hello, world!");
}
}
#694 DnsAddressEndpointGroup
and DnsServiceEndpointGroup
which uses DNS A/AAAA and SRV records respectively.
#696 Media type negotiation for annotated services using @ConsumerType
and @ProducerType
ServerBuilder sb = new ServerBuilder();
sb.annotatedService(new Object() {
@Get("/greet")
@ProduceType("application/json;charset=UTF-8")
public HttpResponse greetGet(@Param("name") String name) {
return HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, "{\"name\":\"%s\"}", name);
}
@Post("/greet")
@ConsumeType("application/x-www-form-urlencoded")
public HttpResponse greetPost(@Param("name") String name) {
return HttpResponse.of(HttpStatus.OK);
}
});
#701 Add StreamMessageDuplicator.duplicateStream(boolean)
for potentially reduced memory footprint and more convenience.
HttpRequestDuplicator dup = new HttpRequestDuplicator(req);
HttpRequest firstDuplicate = dup.duplicateStream();
HttpRequest lastDuplicate = dup.duplicateStream(true);
// No need to call dup.close(); underlying buffered content will be
// removed automatically as the duplicate streams are consumed.
#711 Provide a simpler way to add gRPC services.
GrpcService
now implements a new interface ServiceWithPathMappings
which allows a user not to specify path mapping:
ServerBuilder sb = new ServerBuilder();
sb.service(new GrpcServiceBuilder()
.addService(new MyGrpcServiceOne())
.supportedSerializationFormats(GrpcSerializationFormats.values())
.build())
.service(new GrpcServiceBuilder()
.addService(new MyGrpcServiceTwo())
.addService(new MyGrpcServiceThree())
.supportedSerializationFormats(GrpcSerializationFormats.values())
.build(), authDecorator);
This doesn't work only for GrpcService
but any ServiceWithPathMappings
implementations.
#728 Add session-level options for client-side initial HTTP/2 settings
ClientFactory cf = new ClientFactoryBuilder()
.initialHttp2ConnectionWindowSize(1048576)
.initialHttp2StreamWindowSize(131072)
.http2MaxFrameSize(32768)
.build();
#735 Allow specifying alternative timeout for retry attempts. e.g. Set the timeout of each attempt to 1 second while RetryingClient
will give up 10 seconds after the initial attempt.
// Retry when could not receive a resonse before timeout.
RetryStrategy<HttpRequest, HttpResponse> strategy = new RetryStrategy<HttpRequest, HttpResponse>() {
final Backoff backoff = Backoff.fixed(1000);
@Override
public CompletableFuture<Optional<Backoff>> shouldRetry(HttpRequest request,
HttpResponse response) {
return response.aggregate().handle((result, cause) -> {
if (cause instanceof ResponseTimeoutException) {
return Optional.of(backoff);
}
return Optional.empty();
});
}
};
HttpClient client = new HttpClientBuilder("http://example.com/")
.decorator(RetryingHttpClient.newDecorator(
strategy, /* defaultMatAttempts */ 10, /* responseTimeoutForEachAttempt */ 1000))
.build();
Server.toString()
LoggingService
/greet
for GET and the other at /greet
for POSTHttpResponse
produced by TomcatService
does not contain content-length
header.service(ServiceWithPathMappings,...)
to AbstractVirtualHostBuilder
DeferredStreamMessage
causes a subscription leak.JettyService
fails with IllegalStateException
when ResourceHandler
tries to serve the resource greated than 8192 bytes.CommonPools.bossGroup
Server
doesn't share the daemon thread from bossGroup()
and creates a single-thread boss EventLoopGroup
using newly added method that is EventLoopGroups.newEventLoopGroup(numThreads, threadFactory)
Server
terminates the boss groups lastly so that the JVM does not terminate itself until the graceful shutdown is complete.HttpResponseException(HttpStatus)
to send an error responseServer
creates a dedicated single-thread boss EventLoopGroup
for each ServerPort
in order to prevent premature shutdown as wellAnnotatedServiceRegistrationBeans
is not presentRequestContext
accessible during converting a response by ResponseConverter
#644 Replace SessionOptions
with ClientFactoryBuilder
// Before
ClientFactory factory = new AllInOneClientFactory(SessionOptions.of(
SessionOption.USE_HTTP2_PREFACE.newValue(true),
SessionOption.USE_HTTP1_PIPELINING.newValue(true), ...);
// After
ClientFactory factory = new ClientFactoryBuilder()
.useHttp2Preface(true)
.useHttp1Pipelining(true)
...
.build();
#619 #648 Change the parameters of JitterAddingBackoff
from absolute values to rates
// Before
Backoff backoff = Backoff.exponential(1000, 60000).withJitter(1000); // 1-second jitter
// After
Backoff backoff = Backoff.exponential(1000, 60000).withJitter(0.3); // 30% jitter
#652 Add common EventLoopGroup
s and blocking task executor
Previously, unless specified explicitly, a ClientFactory
and a Server
had their own EventLoopGroup
s. From this release, we provide common EventLoopGroup
s via com.linecorp.armeria.common.CommonPools
so that Armeria does not create unnecessarily large number of event loop threads.
// Before: nCpuCore * 2 * 3 threads (s1, s2 and cf do not share the event loops)
Server s1 = new ServerBuilder()...build();
Server s2 = new ServerBuilder()...build();
ClientFactory cf = new AllInOneClientFactory();
// After: nCpuCore * 2 threads (s1, s2 and cf share the event loops)
Server s1 = new ServerBuilder()...build();
Server s2 = new ServerBuilder()...build();
ClientFactory cf = new ClientFactoryBuilder()...build();
We also added ServerBuilder.worker/bossGroup()
and ClientFactoryBuilder.workerGroup()
so that you can specify an alternative EventLoopGroup
easily. You'll find com.linecorp.armeria.common.util.EventLoopGroups.newEventLoopGroup()
useful as well.
Server s = new ServerBuilder()
// 4 dedicated event loop threads.
// 'true' means 'shut down the event loops when server stops.'
.workerGroup(EventLoopGroups.newEventLoopGroup(4), true)
...
.build();
ClientFactory cf = new ClientFactoryBuilder()
// 2 dedicated event loop threads.
// 'true' means 'shut down the event loops when factory closes.'
.workerGroup(EventLoopGroups.newEventLoopGroup(2), true)
...
.build();
Similarly, we provide common blocking task executor via CommonPools
, which was created per-Server
until this release.
#667 Make StreamMessage
implementations pass Reactive Streams TCK and clarify the behavior of StreamMessage.abort()
StreamMessage.subscribe()
does not raise an exception immediately anymore. The subscriber is notified via onError()
about subscription failure.AbortedStreamException
is signaled to the subscriber's onError()
.#645 #672 Allow more customization of LoggingService
.
Separated log levels for request, response and failure.
Added customizable log sanitization
We changed the log level of failed responses from INFO to WARN.
ServerBuilder sb = new ServerBuilder();
sb.service(..., myService.decorate(new LoggingServiceBuilder()
.requestLogLevel(LogLevel.INFO)
.successfulResponseLogLevel(LogLevel.INFO)
.failureResponseLogLevel(LogLevel.WARN)
.newDecorator()));
#681 #677 Update dependencies
#680 Add HttpClientBuilder
and HttpClient.of()
which allows easier construction of HttpClient
// Before:
HttpClient c1 = Clients.newClient("none+http://example.com/");
HttpClient c2 = new ClientBuilder("none+http://example.com/")
.decorator(HttpRequest.class, HttpResponse.class, LoggingClient.newDecorator())
.build(HttpClient.class);
// After:
// - No need to prepend 'none+'
// - No need to specify HttpRequest.class` and HttpResponse.classs
// - No need to specify HttpClient.class
HttpClient c1 = HttpClient.of("http://example.com/");
HttpClient c2 = new HttpClientBuilder("http://example.com/")
.decorator(LoggingClient.newDecorator())
.build();
#628 #660 Provide a way to specify a Backoff using a string expression
// Useful when you want to make backoff strategy configurable:
Backoff backoff = Backoff.of("exponential=1000:60000,jitter=0.3,maxAttempts=10");
#521 #662 Allow setting a custom request timeout handler
@Get("/hello/:name")
public HttpResponse hello(ServiceRequestContext ctx, @Param("name") name) {
DefaultHttpResponse res = new DefaultHttpResponse();
ctx.setRequestTimeoutHandler(() -> {
res.respond(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8,
"A very late hello, %s.", name);
});
...
}
#657 Add content-security-policy
and x-frame-options
to HttpHeaderNames
DefaultKeyedChannelPool
PathMapping
implementations to produce their loggerName
and metricName
correctlyEventLoop.terminationFuture()
listener leakRetryingClient
#616 Renamed RetryRequestStrategy
to RetryStrategy
.
#616 Renamed Backoff.nextIntervalMillis()
to nextDelayMillis()
.
#631 Removed .http.
from the package names. Use the following command for migration:
find -name '*.java' -exec perl -pi -e \
's/(com\.linecorp\.armeria\.(?:client|common|server|internal)\.)http\./$1/g' {} ';'
#631 HttpSessionProtocols
, which was deprecated in 0.49.0, has been removed.
#616 Added RetryingHttpClient
HttpClient client = new ClientBuilder("none+http://example.com/")
.decorator(HttpRequest.class, HttpResponse.class,
new RetryingHttpClientBuilder(...)...build())
.build(HttpClient.class);
ResponseTimeoutException
.MonitoringConfiguration
? super I, ? extends O
with RpcRequest, RpcResponse
or HttpRequest, HttpResponse
.SessionProtocolProvider
.HttpSessionProtocols
.SessionProtocol
an enum and undeprecate its values.RequestLog
and RequestLogBuilder
more HTTP-friendly.
requestEnvelope()
with requestHeaders()
.responseEnvelope()
with responseHeaders()
.RequestLogAvailability.REQUEST_ENVELOPE
with REQUEST_HEADERS
.RequestLogAvailability.RESPONSE_ENVELOPE
with RESPONSE_HEADERS
.status()
which is HttpStatus
.RequestLogAvailability.STATUS_CODE
because HttpStatus
.responseEnvelope
becomes available.Server
without using the configurator. For example:
public AnnotatedServiceRegistrationBean okService() {
return new AnnotatedServiceRegistrationBean()
.setServiceName("myAnnotatedService")
.setPathPrefix("/my_service")
.setService(new MyAnnotatedService())
.setDecorator(LoggingService.newDecorator());
SslContext
SessionOption.TRUST_MANAGER_FACTORY
is deprecated. Use SessionOption.SSL_CONTEXT_CUSTOMIZER
instead. You can set a TrustManagerFactory
with SslContextBuilder
as follows:
private static final ClientFactory clientFactory =
new AllInOneClientFactory(SessionOptions.of(
SessionOption.SSL_CONTEXT_CUSTOMIZER.newValue(
b -> b.trustManager(InsecureTrustManagerFactory.INSTANCE)),
...
blockingTaskExecutor
is not single-threaded anymore.DeferredStreamMessage.subscribeToDelegate()
.DynamicHttpService
is merged into the core. Also, HTTP parameters are automatically injected as arguments when calling an annotated service method. Your code will fail to compile if you wrote your service using DynamicHttpService
.
DynamicHttpServiceBuilder
class is removed. You can use ServerBuilder.annotatedService(...)
to add an annotated service object to the builder as follows:
ServerBuilder sb = ...;
// Using a query parameter (e.g. /greet5?name=alice) on an annotated service object:
sb.annotatedService(new Object() {
@Get("/greet5")
public HttpResponse greet(@Param("name") String name,
@Param("title") @Optional("Mr.") String title) {
// "Mr." is used by default if there is no title parameter in the request.
return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8,
"Hello, %s %s!", title, name);
}
});
@PathParam
annotation has been replaced with @param
, which is much shorter and handles query parameters as well.@Get
, is changed to com.linecorp.armeria.server.http.annotation
from com.linecorp.armeria.server.http.dynamic
./login
(no path parameters)/users/{userId}
(curly-brace style)/list/:productType/by/:ordering
(colon style)exact:/foo/bar
(exact match)prefix:/files
(prefix match)glob:/~*/downloads/**
(glob pattern)regex:^/files/(?.*)$
(regular expression)PathMappingResult
. ServiceRequestContext.mappedPath()
would return the value from PathMappingResult.path()
.Flags
HttpReqeust
and HttpResponse
RpcRequest
and RpcResponse
StreamMessageDuplicator
. StreamMessage
can be subscribed by multiple subscribers now.AggregatedHttpMessage.toHttpRequest/Response()
ThriftServiceRegistrationBean
ManagedHttpHealthCheckService
, so you can override the healthiness via a PUT request.DefaultStreamMessage
/ Improve the life cycle contract of reference-counted objects in StreamMessage
.DefaultStreamMessage
cleans up the objects even when demand is 0.PathMappingResult
when query string exists.HttpResponse.ofFailed()
to ofFailure()
.armeria-tomcat8.0
and armeria-thrift0.9
.spring-boot-autoconfigure
POM to allow a user to choose Thrift version.DefaultStreamMessage
and improve the life cycle contract of reference-counted objects in StreamMessage
DefaultStreamMessage
cleans up the objects even when demand is 0