tupelo-schneck opened a new pull request, #4226:
URL: https://github.com/apache/logging-log4j2/pull/4226

   `CronTriggeringPolicy.initialize()` spends roughly three seconds per 
appender on startup for any appender configured without a `fileName`. A 
configuration with two such appenders takes about six seconds to initialise 
logging.
   
   ## Cause
   
   `initialize()` looks up the previous fire time relative to 
`RollingFileManager#getFileTime()`:
   
   ```java
   final Date lastRollForFile = cronExpression.getPrevFireTime(new 
Date(this.manager.getFileTime()));
   ```
   
   `RollingFileManager` reports a file time of 0 when there is no current file 
yet (`file == null || !file.exists() ? 0 : initialFileTime(file)`), which is 
the case on every startup of an appender that writes directly to the file its 
pattern resolves to. The lookup therefore runs against the epoch.
   
   `CronExpression.getTimeBefore()` walks backwards from the target one 
`findMinIncrement()` step at a time, calling `getTimeAfter()` until the result 
precedes the target:
   
   ```java
   do {
       final Date prevCheckDate = new Date(start.getTime() - minIncrement);
       prevFireTime = getTimeAfter(prevCheckDate);
       if (prevFireTime == null || prevFireTime.before(MIN_DATE)) {
           return null;
       }
       start = prevCheckDate;
   } while (prevFireTime.compareTo(targetDateNoMs) >= 0);
   ```
   
   `getTimeAfter()` clamps its result to 1970, because an expression without a 
year field gets a year set starting at 1970 and the tail set lookup returns 
that first entry for any earlier year. For a target at the epoch the loop 
condition can therefore never be satisfied: every iteration returns the same 
1970 fire time, which is neither `null`, nor before `MIN_DATE`, nor before the 
target. The search walks back through several millennia of candidate dates 
until the calendar's era flips and `getTimeAfter()` finally bails out at its 
`YEAR > 2999` guard, and the method returns `null`.
   
   A stack sample from an affected startup, taken once a second, showing the 
main thread pinned at 100% CPU for the duration:
   
   ```
   "main" #3 prio=5 os_prio=31 cpu=749.54ms elapsed=103.85s runnable
      java.lang.Thread.State: RUNNABLE
        at 
org.apache.logging.log4j.core.util.CronExpression.getTimeAfter(CronExpression.java:1201)
        at 
org.apache.logging.log4j.core.util.CronExpression.getTimeBefore(CronExpression.java:1584)
        at 
org.apache.logging.log4j.core.util.CronExpression.getPrevFireTime(CronExpression.java:1594)
        at 
org.apache.logging.log4j.core.appender.rolling.CronTriggeringPolicy.initialize(CronTriggeringPolicy.java:67)
        at 
org.apache.logging.log4j.core.appender.rolling.RollingFileManager.initialize(RollingFileManager.java:246)
        at 
org.apache.logging.log4j.core.appender.RollingFileAppender$Builder.build(RollingFileAppender.java:164)
        ...
        at 
org.apache.logging.log4j.core.LoggerContext.start(LoggerContext.java:311)
   ```
   
   Reproduced with:
   
   ```xml
   <RollingFile name="errorLogAppender" 
filePattern="${LOG_DIR}/error.log-%d{yyyyMMdd}">
       <PatternLayout pattern="%m%n"/>
       <CronTriggeringPolicy schedule="0 0 0 ? * SUN" evaluateOnStartup="true"/>
   </RollingFile>
   ```
   
   A weekly schedule is the worst case, since `findMinIncrement()` returns a 
day-sized step for it, but any schedule whose previous fire time cannot be 
resolved against the epoch is affected. Note that `evaluateOnStartup="false"` 
does not avoid the cost, because the lookup at line 67 runs unconditionally and 
only its result is guarded by that flag.
   
   ## Fix
   
   Bound the backward search at `MIN_DATE`, and skip the lookup entirely when 
there is no current file. Both paths already returned `null` for this input, so 
behaviour is unchanged; only the cost differs.
   
   `getPrevFireTime(new Date(0))` for `0 0 0 ? * SUN`, measured against this 
branch's `CronExpression`:
   
   | | duration |
   | --- | --- |
   | before | 2854–3040 ms |
   | after | 0.03–0.06 ms |
   
   End to end, the two-appender configuration above goes from ~6.0 s to ~0.2 s 
to initialise.
   
   ## Testing
   
   Two regression tests are included. Both were confirmed to fail against `2.x` 
without the fix and pass with it:
   
   * `CronExpressionTest#testPrevFireTimeAtEpochReturnsNullPromptly` — asserts 
`null` within a 1 s timeout. The timeout is deliberately set well below the 
~2.9 s the unfixed path takes, since a generous timeout would pass with the 
defect still present.
   * `CronTriggeringPolicyTest#testBuilderWithoutFileNameInitializesPromptly` — 
builds an appender without a `fileName` on a weekly schedule.
   
   `./mvnw verify` passes across all 41 modules: 11089 tests, 0 failures, 0 
errors, 40 pre-existing environment-conditional skips.
   
   ## Checklist
   
   * [x] Based on `2.x`
   * [x] `./mvnw verify` succeeds
   * [x] Changelog entry in `src/changelog/.2.x.x`
   * [x] Tests are provided
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to