Releases: canopy-labs/featureflip-ruby
Release list
v2.9.0
Fixed
- The polling fallback no longer ends the SSE stream. After
max_stream_retries(5) consecutive failures — about 31 seconds of unreachability, so an edge incident, a bad deploy or a network partition — the streaming thread calledon_give_upand exited, and the core cleared its handler, so nothing could ever restart streaming: the process lost real-time updates until it was restarted (orFeatureflip.restartwas called by hand for fork-safety) and polled/v1/sdk/flagsevery 30 seconds indefinitely. Flag changes, kill switches included, then arrived up to a poll interval late. Polling is now additive — it covers the outage while the stream keeps retrying underneath at the capped, jittered backoff, and the next delivered frame retires the poller. The poller is retired from the frame-delivery path rather than when the stream next drops, because a healthy stream blocks in its read loop for its whole lifetime and a poller left running beside it reverts SSE deltas with its own whole-store replaces. (#3071)
v2.8.0
Fixed
-
A condition operator is now recognised however it is spelled:
NotEquals,notequals,NOTEQUALSandnot_equalsall resolve to the same operator. This SDK previously matched the canonical PascalCase label exactly and treated every other spelling as unrecognised — and an unrecognised operator fails closed (#2262), so a mis-spelled operator matched nobody rather than erroring. The go and php SDKs each applied a different rule: go folded case but kept underscores, so it resolvednotequalsand rejectednot_equals, while php inserted underscores ahead of PascalCase runs and did exactly the reverse. Each accepted a form the other refused, so one saved rule could serve different variations to two users purely by which SDK their service ran. All four now normalise by removing underscores and folding case — the one rule that is a superset of all four previous ones, so no SDK gets stricter and no configuration that evaluated before stops doing so. (#2374) -
The operator is resolved once per condition and every subsequent lookup keys off that value, not just the dispatch arm. The operator name also selects the case-sensitivity set and the numeric-coercion set, so normalising at the dispatch alone would have made a mis-cased
MatchesRegexmatch case-insensitively where its canonical spelling does not, and dropped a mis-casedNotEqualsonto the string path, comparing"1"against"1.0"lexically. (#2374) -
The fail-closed guarantee is unchanged and re-asserted: this resolves spellings, it does not invent operators. A name that is not an operator is still unrecognised and still matches nothing before
negatecan invert it. (#2262)
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)