v2.7.0
Changed
-
A
Before/Afterdate operand that matches the ISO grammar but names no real calendar day now matches nothing, where it previously rolled over into the following month.2024-02-30resolved to 2024-03-01,2023-02-29(2023 is not a leap year) to 2023-03-01 and2024-04-31to 2024-05-01 — so a rule evaluated against a date its author never wrote. The evaluation engine and the C#, Go, Python and Java SDKs have always rejected these, so this converges the SDKs rather than making this one an outlier; until now a single saved rule could serve different variations to two users purely by which SDK their service ran. Follows #2480, which pinned the date grammar — an unreal day is inside that grammar, because a character class cannot express "is a real day", so the grammar guard was silent on it. (#2491) -
The leap-year rule is applied in full, including the century exception:
1900-02-29and2100-02-29match nothing (divisible by 100 but not 400), while2000-02-29and2024-02-29continue to match. The check runs on the operand's written date, before any offset is applied, so2024-02-30T00:00:00+05:00is rejected even though it would resolve to 2024-02-29T19:00Z — a date that does exist. (#2491)
If you have a targeting rule using one of these operands, rewrite it as the date you meant. The Management API has rejected an unreal day on write since #2480 (PortableDateOperand round-trips every grammar-matched operand through the engine's parser), so a rule saved from that release onward cannot carry one — only rules saved earlier are affected.
Fixed
-
An explicit
client.flushno longer opens a second drain loop while one is already running. The in-flight latch added for #2456 guarded only the batch-size trigger, so the periodic flush, an explicitclient.flushand a size-triggered flush could enter the loop together — two request streams against an endpoint the backoff gate exists to protect, and worse, a success in one cleared the gate a failure in the other had just armed, re-opening the one-request-per-evaluation behaviour outright. A caller arriving while a drain is running now waits for it and returns, matching the js and node SDKs. Shutdown still bypasses coalescing, because it is the last drain there will ever be. (#2477) -
A
Before/Afterdate operand that resolves outside the representable date range now matches nothing, where it previously resolved to a real instant. The evaluation engine parses withDateTimeOffset.TryParse, so its accepted range is 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999Z and it matches nothing outside that; this SDK resolved past both ends, so a single saved rule served different variations to two users purely by which SDK their service ran. (#2500) -
The two reachable shapes are a year-zero operand and an operand carried out of range by its offset.
0000-01-01is inside the ISO grammar and is a real proleptic date (0000-02-29exists — year 0 is divisible by 400), so neither #2480's grammar guard nor #2491's calendar-day check excluded it. Separately,[0-9]{4}constrains only the written year while a timezone offset moves the resolved instant, so0001-01-01T00:00:00+05:00fell below the floor and9999-12-31T23:59:59-05:00rose above the ceiling from years the grammar allows. The check therefore runs on the resolved instant — deliberately unlike #2491's, which runs on the written date. (#2500) -
The exact boundaries remain accepted:
0001-01-01,0001-01-01T05:00:00+05:00,9999-12-31T23:59:59Zand9999-12-31T18:59:59-05:00all still resolve. (#2500)
If you have a targeting rule using one of these operands, rewrite it as the date you meant. The Management API has rejected them on write since #2480 (PortableDateOperand round-trips every grammar-matched operand through the engine's own parser, so it inherits the range bound), meaning only rules saved before that release can carry one.
- The first SSE reconnect after a healthy stream drops is now jittered to
[d/2, d], like every other backoff level. The drops this absorbs are fleet-wide — a single edge event severs every stream at once — so every client re-entered the backoff together and waited an identical delay, republishing the drop's own synchronisation as a reconnect spike one backoff later. Measured in production: a drop spread across 2.5–3.0 ms produced a reconnect spread of 26–46 ms. The delay never exceeds the previous one and stays strictly positive, so a stream that fails immediately still cannot busy-loop. (#2508)