Repository navigation
Pause usage sharing sends after repeated failures - #36
Merged
Merged
Conversation
Usage sharing in Node has no back-off. Every package is sent while the service is failing, and is lost. This test expects the back-off .NET is being given, at its default settings of ten failed sends within 100 seconds and a first pause of two seconds. After ten failed sends nothing is sent during the pause, and the evidence processed during it is all sent once the pause ends and the service answers, with no more requests processed. The test has its own receiver on another port, which can answer with any status. It fails until the back-off is added.
Node now has the same back-off as .NET, with the same five settings and defaults, as options. - After failuresToEnterRecovery sends fail within failuresWindowSeconds, sending pauses for initialRecoveryDelaySeconds. A send fails when the request errors, has no answer within requestTimeout, or the answer is not 200. - When a pause ends one send is tried. If it fails the delay is multiplied by recoveryMultiplier, up to maxRecoveryDelaySeconds. If it succeeds the back-off ends. - Processing is not affected. Evidence is queued during a pause, and new evidence is dropped only when the queue is full. The new maximumQueueSize option sets the size, by default 1000 or ten times the requested package size if that is larger, as in .NET. The queue had no limit before, which a pause would have needed. - One message is sent at a time, as in .NET and Java, until fewer than requestedPackageSize entries are queued. A timer that does not keep the process running starts sending when a pause ends, so the queue drains without more requests being processed. - With one message at a time, a request that never answered would stop sending, so the new requestTimeout option, 60 seconds by default as in Python, ends such a request and counts it as a failed send. ShareUsageBackOff holds the rules and takes the time from the caller, so its tests set the time. The type declarations list the new options.
This was referenced Nov 7, 2026
Merged
…ma, a throwing request, the clock and the repeat boundary
… the package size, join entries without commas, time pauses on a monotonic clock and share a repeat at the interval
jwrosewell
marked this pull request as ready for review
November 11, 2026 13:23
This was referenced Nov 11, 2026
Draft
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Node's usage sharing had no back-off. While the service was down or
overloaded, every batch was sent and lost, and the queue was unbounded, so a
busy server could hold a message for every request it served. Four smaller
faults sat with it.
shareData.join(), which isjoin(','), so everymessage carried a comma between each
<Device>element.Usage data sent, so a service answering 500 to everything looked like aservice working.
.NET and Go already back off, so the same outage cost Node users their usage
data and cost .NET and Go users nothing.
What the specification says
usage-sharing-element.md, Back-off after failed sends,
at the fixed commit 8aa2de01 on
rc.The configuration options
table at the same commit gives the five recovery options and their defaults
(10, 100 seconds, 2.0 seconds, 300.0 seconds, 2.0), says an implementation
MUST reject a value outside the range given, gives the maximum queue size as
1000 or ten times the minimum entries per message if that is larger, and says
a message holds from the minimum entries per message to twice that number.
Session tracking
at the same commit settles the repeat interval boundary.
Change
shareUsageBackOff.js,
a new class holding the rules and nothing else.
RangeErrorfor a value outside the range.failed(now)records a failure, drops failures older than the window, andwhen the count reaches
failuresToEnterRecoveryempties the list and startsa pause at the initial delay. While a pause is set, a failure is the trial
failing, so the delay is multiplied by the multiplier, capped at the
maximum. It returns the new pause, or zero when the failure started nothing.
succeeded()clears the delay and the resume time and says whether aback-off ended.
remaining(now)gives what is left of the pause.ShareUsageBackOff.now()reads
performance.now(), which is monotonic, so a change to the systemclock cannot lengthen or skip a pause.
shareUsage.js
maximumQueueSizeandrequestTimeoutaresettings of the element.
maximumQueueSizedefaults to 1000 or ten timesrequestedPackageSize, whichever is larger, and a value belowrequestedPackageSizethrows, because the queue could then never hold amessage.
addToShareUsagedrops a new entry when the queue is full and logs awarning for the first one dropped and then once every 1000, never one for
each. The count starts again once the queue has room. Adding is the
processing path, so processing never waits for a pause.
sendShareUsagestarts nothing while a message is in flight, and while apause is running it sets a timer for the end of the pause instead. The
timer is
unrefed, so it never holds the process open, and a pause longerthan about 24 days is waited out in parts.
sendNexttakes at most twicerequestedPackageSizeentries, sends them,and on success sends the next until fewer than
requestedPackageSizeareleft, so a queue that filled during a pause drains at once.
postis the send. It gives the request a timeout, treats an answer otherthan 200 as a failure and logs the status with the first 500 characters of
the content, treats a request that cannot be made or that throws as a
failure, and calls back exactly once. The entries it took are not put back,
as the page says.
join(''), so there is no longer a comma betweenthe
<Device>elements.shareUsageTracker.js
matchcompares with>=rather than>, so evidence first shared exactlyone interval ago is shared again, which is what the session tracking section
quoted above asks for. The time is still only moved on when the repeat is
shared.
The types in
types/shareUsage.d.tsand the newtypes/shareUsageBackOff.d.tsare regenerated to match.
The same change in the other languages
The option names and the five defaults are the same in all three.
specifications#97, which
made the back-off a requirement, names this draft as the Node change. Java
#62 and Python
#53 carry it for those
two languages and are not mine.
Tests
shareUsageBackOff.test.js
runs a local HTTP server on its own port and sets what that server answers, so
nothing leaves the runner. Eleven tests drive the element (pauses then drains
the queue, the delay grows to the maximum, a full queue drops new evidence and
logs without a message per entry, a request with no answer does not stop
sending, a failed answer logs the status and the content, the message has no
text between devices and holds up to twice the package size, a request that
throws counts as failed,
nowfollows the monotonic clock, and invalidsettings are rejected). Six more drive the rules in
ShareUsageBackOffdirectly.
shareUsageTracker.test.js
is new and holds the interval boundary, with
Date.nowmocked, at theinterval less one, at the interval and at the interval plus one, and that a
repeat inside the interval leaves the stored time alone.
Red. Run 43020800406,
on
tmp/node-36-red. The branch isrcat 8b9fc3d with the two test filesand
shareUsageBackOff.jstaken from this head, and nothing else. The newclass file is there because a test file cannot load a module that does not
exist, and nothing in
rccalls it, sorc's element is unchanged. 11 of the21 tests failed, being nine of the eleven that drive the element and two of
the four tracker tests.
The 10 that passed are the six unit tests of the new class, which is the code
under test in them and is present on the red branch,
now follows the monotonic clock, which is also a test of the class, the two tracker testseither side of the boundary that
>and>=answer the same way, andrequest with no answer does not stop sending, which passes onrcbecausercstarts a send on every call toaddToShareUsagewhatever came before.Green. Run 43023230776,
on
tmp/node-36-green, which is this head. 21 of 21 passed.Full build. Run 43021227015,
on
tmp/pr-matrix-36, success. Every one of its sevenSetting 'PullRequestSha'lines reads77c6ab113d61f96a036dc06e648e1c47b24dabf0, which is this head. Configureprinted "Left out 2 macOS entries". The branch it built from was
rcat8b9fc3d, and
rchas moved on since. Nothingrchas taken since touchesthe three files this changes, and the draft merges into the current
rcwith no conflict.
macOS was left out of this draft's full build under the 9 November decision.
The rc head is built with macOS before it merges into main.
What remains
51degrees-node#45
both change
shareUsage.jsin the constructor and in the two methods thatread the evidence, so whichever merges second needs the other merged into it
first. Neither depends on the other for its behaviour.
for a pause and must not send, has nothing to attach to in Node, because
Node elements have no cleanup step.
specifications#97
records that and does not change it.