Skip to content

Migrate Recycler API to log4j-api #1194

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 19 commits into from
Closed

Conversation

jvz
Copy link
Member

@jvz jvz commented Jan 9, 2023

This will allow for reuse of this API in places where ThreadLocal-recycled objects are currently used. This will be beneficial to runtimes where workloads don't correspond to threads such as virtual threads, coroutines, and reactive streams. This adds an optional dependency on JCTools to log4j-api for one of the QueueingRecycler implementations.

Just one of many PRs to break out from #1144.

jvz added 2 commits January 9, 2023 12:06
This adds executions during process-sources and process-test-sources to ensure that modified files are formatted properly.
This will allow for reuse of this API in places where ThreadLocal-recycled objects are currently used. This will be beneficial to runtimes where workloads don't correspond to threads such as virtual threads, coroutines, and reactive streams. This adds an optional dependency on JCTools to log4j-api for one of the QueueingRecycler implementations.
@jvz jvz requested review from vy and ppkarwasz January 9, 2023 18:47
Copy link
Contributor

@ppkarwasz ppkarwasz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if o.a.l.l.util is the right package for this: RecyclerFactory is something a provider might like to implement. Maybe o.a.l.l.spi is more appropriate?

I would also remove the implementations from the exported packages, leaving just RecyclerFactory and the RecyclerFactories meta-factory.

Any ideas on how an implementer would add new factories? ServiceLoader?

@jvz
Copy link
Member Author

jvz commented Jan 9, 2023

Could be pluggable via dependency injection. That's how I was initially using it, so I suppose that could mean moving the concrete implementations to core. Having access to DI from the API module is related to a new API added in the larger PR that this was separated from.

@jvz
Copy link
Member Author

jvz commented Jan 10, 2023

While I was working on LOG4J2-3228, I noticed what you were referring to in the reusable message classes. Yes, I'd like to make the Recycler API available there, though I'll need to consider the SPI here a little more first. I'll continue porting the other changes first.

@jvz jvz added this to the 3.0.0 milestone Jan 13, 2023
jvz added 2 commits January 14, 2023 14:34
Also updates ThreadLocalRecyclerFactory to support nested calls to acquire() by using an ArrayDeque to track potentially more than one object.

Signed-off-by: Matt Sicker <[email protected]>
@jvz
Copy link
Member Author

jvz commented Jan 14, 2023

So I've moved it to the SPI package. As it stands now, there's already a mini configuration spec string you can specify for recyclers based on the existing JTL configuration parsing logic documented here and here for how to specifically make it work for JTL. I expect that to be generally sufficient when using the API by itself without the backend. In the backend, we have the Injector API where it's much easier to bind things.

@jvz jvz added the api Affects the public API label Jan 14, 2023
@jvz
Copy link
Member Author

jvz commented Jan 14, 2023

Looks like some test failures. Still a work in progress I suppose.

jvz added 3 commits January 14, 2023 17:22
- Split cleaner concerns in RecyclerFactory: eager versus lazy cleaning. This makes the Recycler API work naturally with existing StringBuilder post-usage trimming.
- Uses a simpler method for checking for the presence of JCTools. Given that it's an optional (static) dependency, we can compile direct references to it in classes and see if our classes throw LinkageError-related exceptions at runtime when attempting to link to JCTools.
- Rename QueueSupplier to QueueFactory
- Extract a Queues util for easier access to different JCTools queue implementations when available
- Update implementations of AbstractStringLayout to use Recycler API
  - CsvLogEventLayout
  - CsvParameterLayout
  - GelfLayout
  - HtmlLayout
  - Log4j1SyslogLayout
  - Log4j1XmlLayout
  - PatternLayout
  - Rfc5424Layout
  - SyslogLayout

Signed-off-by: Matt Sicker <[email protected]>
@jvz
Copy link
Member Author

jvz commented Jan 16, 2023

Alright, I think I've figured out one key update to RecyclerFactory to make this API natural to use as a replacement for ad hoc ThreadLocal<StringBuilder>-style object reuse. This is to allow for two different types of cleaner functions to be supplied: one for cleaning a recycled instance when acquired, and another for cleaning a recycled instance when it's released. There are plenty of areas in Log4j where it's expected that StringBuilder instances will be trimmed right after being used, so this helps support that given that other areas that already use Recycler expect that the cleaner is lazily applied. Therefore, I've named them "eager" and "lazy" cleaner functions.

I've also gone ahead and abstracted out some Queue-related utilities to more simply construct JCTools queue classes indirectly when available. Will have some commits to push shortly.

@jvz
Copy link
Member Author

jvz commented Jan 16, 2023

@vy care to have a look at my changes to the Recycler API?

final StringBuilder messageBuffer = getMessageStringBuilder();
layout.serialize(event, messageBuffer);
JsonUtils.quoteAsString(messageBuffer, builder);
final StringBuilder messageBuffer = acquireStringBuilder();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting little use case where you can now abuse the recursion support in ThreadLocalRecyclerFactory to get multiple recyclable objects. Already works as expected with the QueueingRecyclerFactory version.

return builder;
}

private static final ThreadLocal<StringBuilder> timestampStringBuilder = new ThreadLocal<>();

private static StringBuilder getTimestampStringBuilder() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also got a kick out of how pointless this buffer is when using the buffer provided by AbstractStringLayout already. The joys of new APIs!

@ppkarwasz
Copy link
Contributor

There is one Recycler usage case that is not covered by this PR: some objects like LogBuilder must be aware of the recycling lifecycle and be able to signal the recycler, when they are available.

In the case of LogBuilder I imagine a sequence like this:

  • the logger acquires a LogBuilder from a Recycler,
  • the recycler calls reset(Runnable release) on the LogBuilder,
  • when the log method on the LogBuilder is called, the LogBuilder runs the release action.

@@ -89,9 +83,21 @@ public static void release(final Message message) { // LOG4J2-1583
}
}

@Override
public void recycle(final Message message) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this new method compare to release(Message)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of merely clearing the message, this also returns the object back to its pool.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and I wasn't able to name this method release because it conflicts with the static method with the same signature.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

release javadoc states the following

This flag is used internally to verify that a reusable message is no longer in use and can be reused.

This sounds to me exactly like what recycle does. I am curious why do we need separate recycle() and release() methods. Can't we simply move the recycle() logic to the bottom of the release()?

private void onPropertiesPresent(final RingBufferLogEvent event, final List<Property> properties) {
final StringMap contextData = getContextData(event);
for (int i = 0, size = properties.size(); i < size; i++) {
final Property prop = properties.get(i);
// List::forEach is garbage-free when using an ArrayList or Arrays.asList
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it? You need to pass a lambda, which needs to be allocated. I think this needs to be reverted along with the switch from List.of() to Arrays.asList() change in LoggerConfig.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discovered the temporary allocation due to our existing GC tests. Lambdas compile down to some sort of synthetic static method on the class it was declared in.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not able to follow. Are you saying that our GC-tests are returning false negatives and in reality JIT optimizes this piece of code to have no allocations?

@jvz jvz requested a review from vy January 22, 2023 22:00
Signed-off-by: Matt Sicker <[email protected]>
@jvz
Copy link
Member Author

jvz commented Jan 22, 2023

Ignore test failures as those are broken in master already.

This simplifies DefaultLogBuilder as reuse is now handled by the recycler. This also adds a place to store a default RecyclerFactory in LoggingSystem.

Signed-off-by: Matt Sicker <[email protected]>
@jvz
Copy link
Member Author

jvz commented Jan 22, 2023

There is one Recycler usage case that is not covered by this PR: some objects like LogBuilder must be aware of the recycling lifecycle and be able to signal the recycler, when they are available.

In the case of LogBuilder I imagine a sequence like this:

  • the logger acquires a LogBuilder from a Recycler,
  • the recycler calls reset(Runnable release) on the LogBuilder,
  • when the log method on the LogBuilder is called, the LogBuilder runs the release action.

Updated to support this. The recycler doesn't even need to reset the LogBuilder as that can be done in a central place already.

@jvz jvz requested a review from ppkarwasz January 22, 2023 23:47
Comment on lines -275 to +271
final LogEvent logEvent = getLogEventFactory().createEvent(rootLogger.getName(), null, Strings.EMPTY,
final LogEvent logEvent = logEventFactory.createEvent(rootLogger.getName(), null, Strings.EMPTY,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the need for extracting a local variable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getter wasn't for a field; it was doing work each time. I realized upon fixing this that Configuration must be non-null at this point, so we can just get it from Configuration::getLogEventFactory (recently added method).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreeing with @jvz here. If a method is not merely a simple field getter (which is a shame that it is still called getBlah), its result should first better be stored in a self-explanatory variable. This greatly helps while stepping in a debugger.

@vy
Copy link
Member

vy commented Feb 2, 2023

the logger acquires a LogBuilder from a Recycler

@ppkarwasz, does the logger already pool returned LogBuilders? If not, I would rather create a follow-up ticket for this.

@vy
Copy link
Member

vy commented Feb 2, 2023

the logger acquires a LogBuilder from a Recycler

@ppkarwasz, does the logger already pool returned LogBuilders? If not, I would rather create a follow-up ticket for this.

Ignore this please. @jvz says he has already implemented it.

Comment on lines +85 to +86
protected final Recycler<LogBuilder> recycler = LoggingSystem.getRecyclerFactory()
.create(() -> new DefaultLogBuilder(this));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a future extension we should probably allow any LogBuilder...

Comment on lines +2742 to +2743
final DefaultLogBuilder builder = (DefaultLogBuilder) recycler.acquire();
return builder.atLevel(Level.OFF);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...so that we don't need this unchecked cast.

I can open a new issue for this, since this PR is getting huge.

Comment on lines +234 to +238
// recycle self
this.level = null;
this.marker = null;
this.throwable = null;
this.location = null;
Copy link
Contributor

@ppkarwasz ppkarwasz Feb 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fail to understand how can the recycler knows that the LogBuilder can be used again?

@vy vy deleted the branch apache:master February 28, 2023 15:03
@vy vy closed this Feb 28, 2023
@vy vy mentioned this pull request Mar 29, 2023
2 tasks
@ppkarwasz ppkarwasz modified the milestones: 3.0.0, 3.0.0-beta1 Feb 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api Affects the public API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants