From 7b32e8491dbd3050ee85cc1aec2b4aa16036d756 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sat, 7 Nov 2026 06:32:40 +0000 Subject: [PATCH 1/4] Test that a repeat is shared at most once in each interval The usage sharing tracker stores the time evidence was first shared and never moves it on. A repeat is held back for the interval after the first share, and after that it is shared on every request until the cache drops it. The specification describes the interval as a sliding window, which Python and Node follow by storing a new time whenever they share a repeat. The test shares evidence, waits past the interval, shares the repeat, and expects a repeat straight after that not to be shared. It fails until the tracker stores the time a repeat is shared. --- .../trackers/ShareUsageTrackerTests.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java index 32550b140..949dd4bd1 100644 --- a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java +++ b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java @@ -40,6 +40,8 @@ import static fiftyone.pipeline.core.Constants.*; import static fiftyone.pipeline.engines.Constants.DEFAULT_SESSION_COOKIE_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.when; @@ -115,6 +117,30 @@ public void ShareUsageTracker_RepeatEvidence_AfterSessionTimeout() throws Interr assertEquals(2, trackedEvents); } + /** + * A repeat is shared at most once in each interval. Sharing a repeat after + * the interval starts a new interval, so a repeat straight after it is not + * shared. + */ + @Test + public void ShareUsageTracker_RepeatEvidence_SharedOncePerInterval() + throws InterruptedException { + long interval = 500; + ShareUsageTracker shareUsageTracker = new ShareUsageTracker( + new CacheConfiguration(new LruPutCache.Builder(), 1000), + interval, + evidenceKeyFilter); + + assertTrue(shareUsageTracker.track(data), + "New evidence was not shared."); + Thread.sleep(interval + 250); + assertTrue(shareUsageTracker.track(data), + "A repeat after the interval was not shared."); + assertFalse(shareUsageTracker.track(data), + "A repeat straight after a repeat was shared was also shared, " + + "so the time it was shared was not stored."); + } + @Test public void ShareUsageTracker_Session_Track() throws InterruptedException { shareUsageTracker = new ShareUsageTracker( From 5ee58c00145e8148ff827a982497e9c17c01a73c Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sat, 7 Nov 2026 06:36:39 +0000 Subject: [PATCH 2/4] Share a repeated evidence set at most once in each interval The tracker now stores the time it shares a repeat in the cached Date, so the interval starts again from that share. A repeat is shared at most once in each interval, which is what the specification's sliding window describes and what Python and Node already do. The check and the update are made under the Date's lock, so two requests cannot both share the same repeat. --- .../fiftyone/trackers/ShareUsageTracker.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTracker.java b/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTracker.java index 33514287d..321afa610 100644 --- a/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTracker.java +++ b/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTracker.java @@ -31,7 +31,9 @@ /** * A tracker used by share usage to attempt to avoid repeatedly sending data - * relating to the same user session. + * relating to the same user session. Evidence that matches an entry is shared + * again once the interval has passed since it was last shared, so a repeat is + * shared at most once in each interval. * @see Specification */ public class ShareUsageTracker extends TrackerBase { @@ -50,7 +52,8 @@ public class ShareUsageTracker extends TrackerBase { * Construct a new instance. * @param configuration the cache configuration to use when building the * internal cache used by this tracker - * @param intervalMillis interval between matched in millis + * @param intervalMillis how long after evidence is shared a repeat of it is + * not shared, in milliseconds * @param filter the {@link EvidenceKeyFilter} that defines the evidence * values to use when creating a key from an {@link FlowData} */ @@ -65,14 +68,22 @@ public ShareUsageTracker( @Override protected Date newValue(FlowData data) { - // Stored meta-data values for this tracker are always true. + // The time the new evidence is shared. return new Date(); } @Override protected boolean match(FlowData data, Date value) { - Date now = new Date(); - return value.getTime() <= (now.getTime() - interval); + // Sharing a repeat stores the time in the cached value, so the next + // interval starts from this share. + long now = System.currentTimeMillis(); + synchronized (value) { + if (value.getTime() <= now - interval) { + value.setTime(now); + return true; + } + return false; + } } @Override From 4cf14b9be4c652dfb2884147bbb0994d5bc025b9 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sat, 7 Nov 2026 23:30:12 +0000 Subject: [PATCH 3/4] Give the tracker tests that do not test expiry an interval no runner reaches The tests shared a 50 millisecond interval, so on a slow runner two requests that should count as one repeat could be more than 50 milliseconds apart and both be tracked. ShareUsageTracker_Session_DoNotTrack failed that way on MacOS_X64_Java_11, with the test taking 2.4 seconds. Only the test that sleeps past the interval to test expiry keeps the short one. ShareUsageTracker_Session_Track no longer sleeps, so the session cookie alone keeps its two requests apart. --- .../trackers/ShareUsageTrackerTests.java | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java index 949dd4bd1..272be49f6 100644 --- a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java +++ b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java @@ -47,7 +47,16 @@ public class ShareUsageTrackerTests { - private long interval; + /** + * An interval no run of these tests comes near, for the tests that do + * not test expiry, so a slow runner cannot make a repeat count as new. + */ + private static final long NO_EXPIRY_INTERVAL_MILLIS = 60 * 60 * 1000; + + /** + * A short interval, for a test that sleeps past it to test expiry. + */ + private static final long EXPIRY_INTERVAL_MILLIS = 50; private EvidenceKeyFilter evidenceKeyFilter; @@ -64,7 +73,6 @@ public void Init() { includedQueryStringParameters = new ArrayList<>(); sessionCookieName = DEFAULT_SESSION_COOKIE_NAME; - interval = 50; evidenceKeyFilter = new EvidenceKeyFilterShareUsage( blockedHttpHeaders, includedQueryStringParameters, @@ -75,7 +83,7 @@ public void Init() { 1000); shareUsageTracker = new ShareUsageTracker( cacheConfig, - interval, + NO_EXPIRY_INTERVAL_MILLIS, evidenceKeyFilter); evidenceData = new HashMap<>(); @@ -104,6 +112,10 @@ public void ShareUsageTracker_RepeatEvidence_BeforeSessionTimeout() { @Test public void ShareUsageTracker_RepeatEvidence_AfterSessionTimeout() throws InterruptedException { + ShareUsageTracker shareUsageTracker = new ShareUsageTracker( + new CacheConfiguration(new LruPutCache.Builder(), 1000), + EXPIRY_INTERVAL_MILLIS, + evidenceKeyFilter); int trackedEvents = 0; for (int i = 0; i < 2; i++) { if (shareUsageTracker.track(data)) { @@ -111,7 +123,7 @@ public void ShareUsageTracker_RepeatEvidence_AfterSessionTimeout() throws Interr } // Wait some time equal to the interval to elapse. - Thread.sleep(interval); + Thread.sleep(EXPIRY_INTERVAL_MILLIS); } assertEquals(2, trackedEvents); @@ -142,10 +154,12 @@ public void ShareUsageTracker_RepeatEvidence_SharedOncePerInterval() } @Test - public void ShareUsageTracker_Session_Track() throws InterruptedException { + public void ShareUsageTracker_Session_Track() { + // The interval does not expire, so only the session cookie keeps the + // two requests apart. shareUsageTracker = new ShareUsageTracker( new CacheConfiguration(new LruPutCache.Builder(), 1000), - interval, + NO_EXPIRY_INTERVAL_MILLIS, evidenceKeyFilter); int trackedEvents = 0; @@ -164,7 +178,6 @@ public DataKey answer(InvocationOnMock invocationOnMock) throws Throwable { } } ); - Thread.sleep(interval); if (shareUsageTracker.track(data)) { trackedEvents++; } @@ -182,7 +195,7 @@ public void ShareUsageTracker_Session_DoNotTrack() { sessionCookieName); ShareUsageTracker shareUsageTracker = new ShareUsageTracker( new CacheConfiguration(new LruPutCache.Builder(), 1000), - interval, + NO_EXPIRY_INTERVAL_MILLIS, evidenceKeyFilter); int trackedEvents = 0; From d6786f9bf9b3a7b46e0ca049f524ddb444cb7477 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 21:38:52 +0000 Subject: [PATCH 4/4] Wait past the interval with a margin in the repeat evidence test --- .../fiftyone/trackers/ShareUsageTrackerTests.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java index 37e5374fc..eaf8670d8 100644 --- a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java +++ b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/trackers/ShareUsageTrackerTests.java @@ -58,6 +58,12 @@ public class ShareUsageTrackerTests { */ private static final long EXPIRY_INTERVAL_MILLIS = 50; + /** + * Added to the wait for an interval to pass, so that a sleep that returns + * early or a slow clock cannot leave a repeat inside the interval. + */ + private static final long EXPIRY_MARGIN_MILLIS = 250; + private EvidenceKeyFilter evidenceKeyFilter; private ShareUsageTracker shareUsageTracker; @@ -122,8 +128,8 @@ public void ShareUsageTracker_RepeatEvidence_AfterSessionTimeout() throws Interr trackedEvents++; } - // Wait some time equal to the interval to elapse. - Thread.sleep(EXPIRY_INTERVAL_MILLIS); + // Wait for the interval to pass, with a margin. + Thread.sleep(EXPIRY_INTERVAL_MILLIS + EXPIRY_MARGIN_MILLIS); } assertEquals(2, trackedEvents); @@ -145,7 +151,7 @@ public void ShareUsageTracker_RepeatEvidence_SharedOncePerInterval() assertTrue(shareUsageTracker.track(data), "New evidence was not shared."); - Thread.sleep(interval + 250); + Thread.sleep(interval + EXPIRY_MARGIN_MILLIS); assertTrue(shareUsageTracker.track(data), "A repeat after the interval was not shared."); assertFalse(shareUsageTracker.track(data),