Skip to content

implement last-known-good fallback for dynamic limit settings - #2339

Open
tarcisiozf wants to merge 4 commits into
mainfrom
DEVSVCS-5859/limits-last-known-good
Open

implement last-known-good fallback for dynamic limit settings#2339
tarcisiozf wants to merge 4 commits into
mainfrom
DEVSVCS-5859/limits-last-known-good

Conversation

@tarcisiozf

@tarcisiozf tarcisiozf commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

This pull request improves the behavior and documentation of the settings limiters to ensure that, on a settings read failure, the limiters return the compiled default value alongside the error, rather than discarding the value. This makes the error advisory rather than fatal, allowing callers to proceed with a sensible default even when the settings service is unavailable. The changes also add comprehensive tests to verify this behavior across all limiter types.

Behavioral improvements to limiter error handling:

  • All limiter Limit methods (boundLimiter, rangeLimiter, gateLimiter, timeLimiter) now return the resolved value (including the compiled default on error) alongside any error, rather than discarding the value on a settings read failure. This ensures callers can always use the returned value unless the tenant is missing.
  • The WithTimeout method in timeLimiter now returns a usable context and the resolved default timeout even when a settings read fails, with the error being advisory.

Documentation updates:

  • The Limiter interface documentation is updated to clarify that returned values are always usable even when an error is present, except when the tenant is missing.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📊 API Diff Results

No changes detected for module github.com/smartcontractkit/chainlink-common

View full report

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request updates the settings-based limiters to prefer a “last known good” (most recently resolved) value when settings reads fail, instead of always falling back to the compiled default, and adds tests to validate the new fallback behavior.

Changes:

  • Added atomic “last known good” storage to the shared updater and used it during polling/subscription failures.
  • Updated bound/gate/range/time limiters to return usable values even when a settings read fails, with the error treated as advisory (except for missing required tenant context).
  • Added new unit tests covering last-known-good fallback and compiled-default fallback when no successful read has occurred.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/settings/limits/updater.go Stores and reuses last known good values on update failures; logging clarified.
pkg/settings/limits/time.go Makes WithTimeout/Limit return usable values/contexts on read failure while returning the error as advisory.
pkg/settings/limits/range.go Returns last known good range on read failure (or compiled default if none yet).
pkg/settings/limits/gate.go Returns last known good gate value on read failure (or compiled default if none yet).
pkg/settings/limits/bound.go Returns last known good bound on read failure (or compiled default if none yet).
pkg/settings/limits/default_fallback_test.go Adds tests validating last-known-good vs compiled-default fallback behavior across limiter types.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/settings/limits/updater.go
@tarcisiozf
tarcisiozf force-pushed the DEVSVCS-5859/limits-last-known-good branch from fe664ba to 2206913 Compare August 25, 2026 14:31
@tarcisiozf tarcisiozf changed the title change limits implement last-known-good fallback for dynamic limit settings Aug 25, 2026
@tarcisiozf
tarcisiozf marked this pull request as ready for review August 25, 2026 14:31
@tarcisiozf
tarcisiozf requested a review from a team as a code owner August 25, 2026 14:31
Comment thread pkg/settings/limits/updater.go Outdated
Comment thread pkg/settings/limits/bound.go Outdated
Comment on lines +195 to 198
if err != nil {
return nil, nil, err // no tenant: get() never resolved a value
}
return ctx, func() {}, nil // fail open

@jmank88 jmank88 Aug 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't there more to consider here? timeout may be set (or is even always set?), regardless of whether err is nil or tenant is empty, so shouldn't we be using it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this whole short circuit block be droppped?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem in this cases is that the "no-tenant path" leaves timeout at zero, but Limit() intentionally returns -1 there to distinguish from "a real 0s timeout." So fail-open branch would apply an already-expired deadline instead of leaving the context unbounded

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, why is it important to have meaningful 0 vs. -1 cases?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've checked again and it won't matter for Limit(), as the caller already handles it (like limit <= 0). Will clean up this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But for WithTimeout it's a different case, the value is passed to a context.WithTimeoutCause, so the returned context is already expired. That is a fail-closed (cancel immediatly) instead of current fail-open (run unbounded) behavior, which differs from what the original code comment said.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well why does Limit return -1? Should it be returning the timeout value alongside the error instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I believe part of the inconsistency in the "fail open" behavior is because some things are trivial to apply a default to (like this case) while others are complex data structures that must be indexed on the tenant value, meaning we don't really have a clean "default" fallback behavior. We should probably consider using a default rather than unbounded in this case 🤔

Comment thread pkg/settings/limits/limits.go Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants