Ratpack works with a non-blocking event driven model.
Blocking operations such as database access must be done in a separate thread. It's hard to do it by hand, so Ratpack comes with a Promise
callback mechanism and Blocking
utility support for multithreading.
That's all you need to do with "Oh, it feels like JavaScript", but when you use it with other frameworks, there are some problems.
Java Servlet, the standard Java web framework, runs on a 1-request-1-thread model. The Spring Framework is also servlet-based, so it adheres to this model. When the Servlet receives an HTTP request, it creates a new thread (or gets it from the pool) and executes the handler method. This mechanism allows each request to be executed in a separate thread, allowing programmers to write programs (basically) without having to worry about thread safety, such as sharing resources between threads. However, Ratpack is a non-blocking framework, so all requests are executed in the same event loop and in the same thread (exactly a little different, but most images look like that). Since the blocking process is executed in another thread, one request may not be completed in one thread. I think most web applications connect to databases and call external APIs. In summary, in Servlet and Ratpack, the relationship between the execution "context" and the thread does not correspond.
It's a difference in design concept in itself, and it's not a story of which is better or worse, but since the de facto standard Java Servlet is a 1 request-1 thread model, many Java frameworks are built on that assumption. Has been done. Therefore, there is a situation that it cannot be used together with Ratpack as it is.
ThreadLocal
(If you think ThreadLocal
is common sense, go to the next paragraph!)
A typical example is ThreadLocal
. Thread local is a class for holding an instance unique to each thread. Consider ThreadLocalRandom
as an example.
In fact, the Java Random
class is thread-safe. It is safe to share with multiple threads. However, because they are synchronized, performance will suffer if there is a conflict.
This is where ThreadLocalRandom
comes into play. When threads A and B access the same ThreadLocalRandom
at the same time, thread A uses the internal Random
specific to thread A, and thread B uses a separate Random
dedicated to thread B. .. Performance does not deteriorate because there is no synchronization. It's also more efficient than creating a new Random
instance every time on the fly.
With ThreadLocal
, you can use classes that don't support multithreading without worrying about synchronization.
As mentioned at the beginning, Servlets run with 1 request-1 thread. Since ThreadLocal
is unique to each thread, you can actually think of it as" ThreadLocal
has one instance per request." This was a very convenient design for managing the execution context of a web application. For example, if you store user information thread-local, ** the program can consider that thread-local user information as information for the current request **.
A typical example is MDC of logback. If you save the user information in the MDC when the request is first received, the user information can be used together in the subsequent log output. The log output side can use that information without being aware of which request is calling it. It's convenient.
As you may have noticed, Ratpack is non-blocking. One request is not executed in one thread. ** ThreadLocal
cannot be used on Ratpack. ** **
Let's take a concrete example.
MDC.put("user", "John Smith");
logger.info("[1]")
Blocking.op(() -> {
logger.info("[2]");
}).then(() -> {
logger.info("[3]")
ctx.render( "OK" );
});
If it is a Servlet, all three log outputs refer to the same MDC. In all log outputs, the MDC's ʻuser value will be
John Smith. In Ratpack, the processing in
Blocking.op ()is a separate thread. It is a separate thread from the thread that called
MDC.put (). The ʻuser
of[2]
will be null
.
Execution
So how do you manage the context like you did in a Servlet with Ratpack?
In fact, Ratpack executes processing in units of [ʻExecution](https://ratpack.io/manual/current/api/ratpack/exec/Execution.html). The rule that Ratpack's
Promise can only be used inside a Ratpack application was actually" only inside ʻExecution
".
ʻExecution is also the context of the process. The Ratpack application manages the context in ʻExecution
, just as a thread is the execution context in a Servlet. ʻExecutionhas a dictionary function that can be used inside it, and you can save and retrieve instances of a certain type. You can take advantage of this instead of
ThreadLocal`.
I mentioned that ThreadLocal
cannot be used with Ratpack, but Ratpack has MDCInterceptor
for using MDC. ) Is prepared in advance. It's easy to use, just register this class in Registry
.
MDCInterceptor
is an implementation of the [ʻExecInterceptor](https://ratpack.io/manual/current/api/ratpack/exec/ExecInterceptor.html) interface. As the name implies, this interface acts as an interceptor for ʻExecution
. That is, it is used to insert some processing before and after the execution of a certain ʻExecution`.
If you look at the implementation of MDCInterceptor
, you can see that the process is roughly as follows.
MDC.setContextMap ()
to set the retrieved dictionary.MDC.getCopyOfContextMap ()
) and set it to ʻExecution`In short, before the actual processing, the MDC of SLF4J is operated so that the ʻExecution` of Ratpack and the MDC match.
I will finally get into the main subject. The Java client of the log monitoring service Sentry also requires some operations in Ratpack.
Sentry has functions such as Tag and Breadcrumb that record and output the execution context. However, as stated in the Sentry documentation, what "context" means depends on the application. Sentry abstracts this context management with a class called ContextManager
. By default, two implementations are provided, the Java client for Android uses SingletonContextManager
, and the regular version uses ThreadLocalContextManager
. As the name implies, it is singleton and thread local. And, as we've discussed, thread-local isn't available in Ratpack. In the singleton implementation, the user information will be incorrect.
Therefore, I will write a unique implementation of ContextManager
that uses ʻExecution`.
@Override
public Context getContext() {
Optional<Execution> currentExecOpt = Execution.currentOpt();
if ( currentExecOpt.isPresent() ) {
Execution currentExec = currentExecOpt.get();
Optional<Context> context = currentExec.maybeGet( Context.class );
if ( context.isPresent() ) {
return context.get();
} else {
Context newContext = new Context();
currentExec.add( newContext );
return newContext;
}
} else {
return singletonContext;
}
}
First, make sure that the current program is running inside ʻExecution. Returns a singleton
Context object if outside of ʻExecution
. This can be thread-local, but I think that the code that is executed outside ʻExecutionis only the server startup process unless you create a thread yourself. The
Context itself is
synchronized` and thread-safe, so I decided to implement it like this.
If you were in ʻExecution, make sure that the current ʻExecution
contains the Sentry's Context
object. If it is not saved, create a new Context
and register it in ʻExecution. Now the
Context will be inherited by the subsequent ʻExecution
. You can now get the same Context
even if you move to another thread with Blocking
etc.
All we have to do now is use this ContextManager
implementation, which can be achieved by overriding the SentryClientFactory
.
public final class RatpackSentryClientFactory extends DefaultSentryClientFactory {
@Override
protected ContextManager getContextManager( Dsn dsn ) {
return new RatpackSentryContextManager();
}
}
Only this.
Finally, you need to specify this class as the default way to create a Sentry client. There are two ways to do this: using the Sentry.init ()
method and using properties.
Sentry.init( new RatpackSentryClientFactory() );
However, if you use a client that is integrated with other logging frameworks like sentry-logback
, I think Sentry initialization depends on the global Sentry
class. In fact, sentry-logback
is not designed to specify a separate SentryClient
. Therefore, I think it is better to set it via the properties file.
sentry.properties
factory=factory=rip.deadcode.ratpack.sentry.RatpackSentryClientFactory
Specify the FQCN of the created SentryClientFactory
in the value of the factory
key.
ThreadLocal
in RatpackRecommended Posts