diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/DebugListener.java b/jetty-server/src/main/java/org/eclipse/jetty/server/DebugListener.java index 070dc8a15112..e56a8eb0d722 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/DebugListener.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/DebugListener.java @@ -185,7 +185,7 @@ protected void log(String format, Object... arg) long now = System.currentTimeMillis(); long ms = now % 1000; if (_out != null) - _out.printf("%s.%03d:%s%n", __date.formatNow(now), ms, s); + _out.printf("%s.%03d:%s%n", __date.format(now), ms, s); if (LOG.isDebugEnabled()) LOG.debug(s); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java index fd8360ff43f9..942c00691670 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java @@ -104,7 +104,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques private void print(String name, String message) { long now = System.currentTimeMillis(); - final String d = _date.formatNow(now); + final String d = _date.format(now); final int ms = (int)(now % 1000); _print.println(d + (ms > 99 ? "." : (ms > 9 ? ".0" : ".00")) + ms + ":" + name + " " + message); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java b/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java index 65c4f476b780..fc05de1fc4f2 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java @@ -15,55 +15,83 @@ import java.time.Instant; import java.time.ZoneId; -import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.Locale; import java.util.TimeZone; /** - * Date Format Cache. - * Computes String representations of Dates and caches - * the results so that subsequent requests within the same second - * will be fast. - * - * Only format strings that contain either "ss". Sub second formatting is - * not handled. - * - * The timezone of the date may be included as an ID with the "zzz" - * format string or as an offset with the "ZZZ" format string. - * + * Computes String representations of Dates then caches the results so + * that subsequent requests within the same second will be fast. + *
* If consecutive calls are frequently very different, then this * may be a little slower than a normal DateFormat. + *
+ * @see DateTimeFormatter for date formatting patterns.
*/
public class DateCache
{
public static final String DEFAULT_FORMAT = "EEE MMM dd HH:mm:ss zzz yyyy";
private final String _formatString;
- private final String _tzFormatString;
- private final DateTimeFormatter _tzFormat;
- private final Locale _locale;
+ private final DateTimeFormatter _tzFormat1;
+ private final DateTimeFormatter _tzFormat2;
private final ZoneId _zoneId;
- private volatile Tick _tick;
+ private volatile TickHolder _tickHolder;
+
+ private static class TickHolder
+ {
+ public TickHolder(Tick t1, Tick t2)
+ {
+ tick1 = t1;
+ tick2 = t2;
+ }
+
+ final Tick tick1;
+ final Tick tick2;
+ }
public static class Tick
{
- final long _seconds;
- final String _string;
+ private final long _seconds;
+ private final String _prefix;
+ private final String _suffix;
- public Tick(long seconds, String string)
+ public Tick(long seconds, String prefix, String suffix)
{
_seconds = seconds;
- _string = string;
+ _prefix = prefix;
+ _suffix = suffix;
+ }
+
+ public long getSeconds()
+ {
+ return _seconds;
+ }
+
+ public String format(long inDate)
+ {
+ if (_suffix == null)
+ return _prefix;
+
+ long ms = inDate % 1000;
+ StringBuilder sb = new StringBuilder();
+ sb.append(_prefix);
+ if (ms < 10)
+ sb.append("00").append(ms);
+ else if (ms < 100)
+ sb.append('0').append(ms);
+ else
+ sb.append(ms);
+ sb.append(_suffix);
+ return sb.toString();
}
}
/**
- * Constructor.
- * Make a DateCache that will use a default format. The default format
- * generates the same results as Date.toString().
+ * Make a DateCache that will use a default format.
+ * The default format generates the same results as Date.toString().
*/
public DateCache()
{
@@ -71,8 +99,7 @@ public DateCache()
}
/**
- * Constructor.
- * Make a DateCache that will use the given format
+ * Make a DateCache that will use the given format.
*
* @param format the format to use
*/
@@ -93,56 +120,44 @@ public DateCache(String format, Locale l, String tz)
public DateCache(String format, Locale l, TimeZone tz)
{
+ this(format, l, tz, true);
+ }
+
+ public DateCache(String format, Locale l, TimeZone tz, boolean subSecondPrecision)
+ {
+ format = format.replaceFirst("S+", "SSS");
_formatString = format;
- _locale = l;
+ _zoneId = tz.toZoneId();
- int zIndex = _formatString.indexOf("ZZZ");
- if (zIndex >= 0)
+ String format1 = format;
+ String format2 = null;
+ boolean subSecond;
+ if (subSecondPrecision)
{
- final String ss1 = _formatString.substring(0, zIndex);
- final String ss2 = _formatString.substring(zIndex + 3);
- int tzOffset = tz.getRawOffset();
-
- StringBuilder sb = new StringBuilder(_formatString.length() + 10);
- sb.append(ss1);
- sb.append("'");
- if (tzOffset >= 0)
- sb.append('+');
- else
+ int msIndex = format.indexOf("SSS");
+ subSecond = (msIndex >= 0);
+ if (subSecond)
{
- tzOffset = -tzOffset;
- sb.append('-');
+ format1 = format.substring(0, msIndex);
+ format2 = format.substring(msIndex + 3);
}
-
- int raw = tzOffset / (1000 * 60); // Convert to seconds
- int hr = raw / 60;
- int min = raw % 60;
-
- if (hr < 10)
- sb.append('0');
- sb.append(hr);
- if (min < 10)
- sb.append('0');
- sb.append(min);
- sb.append('\'');
-
- sb.append(ss2);
- _tzFormatString = sb.toString();
}
else
- _tzFormatString = _formatString;
-
- if (_locale != null)
{
- _tzFormat = DateTimeFormatter.ofPattern(_tzFormatString, _locale);
+ subSecond = false;
+ format1 = format.replace("SSS", "000");
}
+
+ _tzFormat1 = createFormatter(format1, l, _zoneId);
+ _tzFormat2 = subSecond ? createFormatter(format2, l, _zoneId) : null;
+ }
+
+ private DateTimeFormatter createFormatter(String format, Locale locale, ZoneId zoneId)
+ {
+ if (locale == null)
+ return DateTimeFormatter.ofPattern(format).withZone(zoneId);
else
- {
- _tzFormat = DateTimeFormatter.ofPattern(_tzFormatString);
- }
- _zoneId = tz.toZoneId();
- _tzFormat.withZone(_zoneId);
- _tick = null;
+ return DateTimeFormatter.ofPattern(format, locale).withZone(zoneId);
}
public TimeZone getTimeZone()
@@ -152,92 +167,90 @@ public TimeZone getTimeZone()
/**
* Format a date according to our stored formatter.
+ * If it happens to be in the same second as the last
+ * formatNow call, then the format is reused.
*
- * @param inDate the Date
- * @return Formatted date
+ * @param inDate the Date.
+ * @return Formatted date.
*/
public String format(Date inDate)
{
- long seconds = inDate.getTime() / 1000;
-
- Tick tick = _tick;
-
- // Is this the cached time
- if (tick == null || seconds != tick._seconds)
- {
- return ZonedDateTime.ofInstant(inDate.toInstant(), _zoneId).format(_tzFormat);
- }
-
- return tick._string;
+ return format(inDate.getTime());
}
/**
* Format a date according to our stored formatter.
- * If it happens to be in the same second as the last formatNow
- * call, then the format is reused.
+ * If it happens to be in the same second as the last
+ * formatNow call, then the format is reused.
*
- * @param inDate the date in milliseconds since unix epoch
- * @return Formatted date
+ * @param inDate the date in milliseconds since unix epoch.
+ * @return Formatted date.
*/
public String format(long inDate)
{
- long seconds = inDate / 1000;
-
- Tick tick = _tick;
-
- // Is this the cached time
- if (tick == null || seconds != tick._seconds)
- {
- // It's a cache miss
- return ZonedDateTime.ofInstant(Instant.ofEpochMilli(inDate), _zoneId).format(_tzFormat);
- }
+ return formatTick(inDate).format(inDate);
+ }
- return tick._string;
+ /**
+ * Format a date according to supplied formatter.
+ *
+ * @param inDate the date in milliseconds since unix epoch.
+ * @return Formatted date.
+ */
+ protected String doFormat(long inDate, DateTimeFormatter formatter)
+ {
+ if (formatter == null)
+ return null;
+ return formatter.format(Instant.ofEpochMilli(inDate));
}
/**
* Format a date according to our stored formatter.
* The passed time is expected to be close to the current time, so it is
* compared to the last value passed and if it is within the same second,
- * the format is reused. Otherwise a new cached format is created.
+ * the format is reused. Otherwise, a new cached format is created.
*
* @param now the milliseconds since unix epoch
* @return Formatted date
+ * @deprecated use {@link #format(long)}
*/
+ @Deprecated
public String formatNow(long now)
{
- long seconds = now / 1000;
-
- Tick tick = _tick;
-
- // Is this the cached time
- if (tick != null && tick._seconds == seconds)
- return tick._string;
- return formatTick(now)._string;
+ return format(now);
}
+ @Deprecated
public String now()
{
return formatNow(System.currentTimeMillis());
}
+ @Deprecated
public Tick tick()
{
return formatTick(System.currentTimeMillis());
}
- protected Tick formatTick(long now)
+ protected Tick formatTick(long inDate)
{
- long seconds = now / 1000;
+ long seconds = inDate / 1000;
- Tick tick = _tick;
- // recheck the tick, to save multiple formats
- if (tick == null || tick._seconds != seconds)
+ // Two Ticks are cached so that for monotonically increasing times to not see any jitter from multiple cores.
+ // The ticks are kept in a volatile field, so there a small risk of inconsequential multiple recalculations
+ TickHolder holder = _tickHolder;
+ if (holder != null)
{
- String s = ZonedDateTime.ofInstant(Instant.ofEpochMilli(now), _zoneId).format(_tzFormat);
- _tick = new Tick(seconds, s);
- tick = _tick;
+ if (holder.tick1 != null && holder.tick1.getSeconds() == seconds)
+ return holder.tick1;
+ if (holder.tick2 != null && holder.tick2.getSeconds() == seconds)
+ return holder.tick2;
}
+
+ String prefix = doFormat(inDate, _tzFormat1);
+ String suffix = doFormat(inDate, _tzFormat2);
+ Tick tick = new Tick(seconds, prefix, suffix);
+ _tickHolder = new TickHolder(tick, (holder == null) ? null : holder.tick1);
return tick;
}
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java
index 36b8da4fd3de..e9d27721b006 100644
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java
@@ -14,15 +14,22 @@
package org.eclipse.jetty.util;
import java.time.Instant;
+import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class DateCacheTest
@@ -39,7 +46,7 @@ public void testDateCache() throws Exception
Instant now = Instant.now();
Instant end = now.plusSeconds(3);
- String f = dc.formatNow(now.toEpochMilli());
+ String f = dc.format(now.toEpochMilli());
int hits = 0;
int misses = 0;
@@ -47,7 +54,7 @@ public void testDateCache() throws Exception
while (now.isBefore(end))
{
String last = f;
- f = dc.formatNow(now.toEpochMilli());
+ f = dc.format(now.toEpochMilli());
// System.err.printf("%s %s%n",f,last==f);
if (last == f)
hits++;
@@ -65,16 +72,11 @@ public void testAllMethods()
{
// we simply check we do not have any exception
DateCache dateCache = new DateCache();
- assertNotNull(dateCache.formatNow(System.currentTimeMillis()));
- assertNotNull(dateCache.formatNow(new Date().getTime()));
- assertNotNull(dateCache.formatNow(Instant.now().toEpochMilli()));
-
- assertNotNull(dateCache.format(new Date()));
- assertNotNull(dateCache.format(new Date(System.currentTimeMillis())));
-
assertNotNull(dateCache.format(System.currentTimeMillis()));
assertNotNull(dateCache.format(new Date().getTime()));
assertNotNull(dateCache.format(Instant.now().toEpochMilli()));
+ assertNotNull(dateCache.format(new Date()));
+ assertNotNull(dateCache.format(new Date(System.currentTimeMillis())));
assertNotNull(dateCache.formatTick(System.currentTimeMillis()));
assertNotNull(dateCache.formatTick(new Date().getTime()));
@@ -88,4 +90,61 @@ public void testAllMethods()
assertNotNull(dateCache.tick());
}
+
+ @Test
+ public void testChangeOfSecond()
+ {
+ AtomicInteger counter = new AtomicInteger();
+ DateCache dateCache = new DateCache(DateCache.DEFAULT_FORMAT + " | SSS", null, TimeZone.getTimeZone("UTC"))
+ {
+ @Override
+ protected String doFormat(long inDate, DateTimeFormatter formatter)
+ {
+ counter.incrementAndGet();
+ return super.doFormat(inDate, formatter);
+ }
+ };
+
+
+ for (int i = 0; i < 10; i++)
+ {
+ assertThat(format(dateCache, "2012-12-21T10:15:30.55Z"), equalTo("Fri Dec 21 10:15:30 UTC 2012 | 550"));
+ assertThat(format(dateCache, "2012-12-21T10:15:31.33Z"), equalTo("Fri Dec 21 10:15:31 UTC 2012 | 330"));
+ }
+
+ // We have 4 formats, two for each second, suffix and prefix.
+ assertThat(counter.get(), equalTo(4));
+ }
+
+ static Stream