[JAVA] Introduction to Ratpack (Extra Edition) --Using Sentry

Thread-based vs event-driven

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.

Context crisis

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 calledMDC.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 ofThreadLocal`.

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.

  1. Extract the context dictionary from ʻExecution`. If not, create an empty context dictionary.
  2. Call MDC.setContextMap () to set the retrieved dictionary.
  3. Execute ʻExecution`.
  4. Extract the context dictionary from the MDC (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.

Use Sentry

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. TheContext 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.

Summary

Recommended Posts

Introduction to Ratpack (Extra Edition) --Using Sentry
Introduction to Ratpack (Extra Edition) --Ratpack written in Kotlin
Introduction to Ratpack (8)-Session
Introduction to Ratpack (6) --Promise
Introduction to Ratpack (2)-Architecture
Introduction to Ratpack (7) --Guice & Spring
Introduction to Ratpack (1) --What is Ratpack?
Introduction to Ratpack (3) --hello world detailed explanation
Introduction to Ruby 2
Introduction to SWING
Introduction to web3j
Introduction to Micronaut 1 ~ Introduction ~
[Java] Introduction to Java
Introduction to migration
Introduction to java
Introduction to Doma
[Introduction to Spring Boot] Submit a form using thymeleaf
Introduction to JAR files
Introduction to RSpec 1. Test, RSpec
Introduction to bit operation
Introduction to PlayFramework 2.7 ① Overview
Introduction to Android Layout
Introduction to design patterns (introduction)
Introduction to Practical Programming
Introduction to javadoc command
Introduction to jar command
Introduction to lambda expression
Introduction to java command
Introduction to RSpec 2. RSpec setup
Introduction to Keycloak development
Introduction to javac command
[Xcode 12.3] Introduction to Apple Platform App Development-Object Edition- [Swift UI]
[Rails] Integration test using Capybara (from introduction to simple test execution)