-
Notifications
You must be signed in to change notification settings - Fork 48
feat: Remove permanent failure modes from FDv1 following RETRY spec #519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jsonbailey
wants to merge
16
commits into
main
Choose a base branch
from
jb/sdk-2792/retry-conformance
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
ae7581a
feat: Remove permanent failure modes from FDv1 following RETRY spec
jsonbailey fa8d44b
cleanup and simplify based on feedback
jsonbailey 9413155
reset attempt count on reset
jsonbailey 30c2f08
refactor: Report the next delay through the property, not a return value
jsonbailey 23474a7
chore: Remove spec citations and passthrough test helpers
jsonbailey fbd8f85
fix: Address review findings on input guards, next delay and test cov…
jsonbailey da72713
addressing feedback
jsonbailey 619b21e
fix: Privatize retry state internals and remove the streaming delay f…
jsonbailey e51858c
Merge remote-tracking branch 'origin/main' into jb/sdk-2792/retry-con…
jsonbailey cd3dd63
fix: Validate the configured data source intervals in Config
jsonbailey 1d0cf93
refactor: Give the spec's attempts name to the counter that drives th…
jsonbailey ea5d3c5
Merge remote-tracking branch 'origin/main' into jb/sdk-2792/retry-con…
jsonbailey 1b50827
docs: Correct what Config checks in the retry factory docstrings
jsonbailey e1f29df
fix: Validate data source intervals in the retry state and bound the …
jsonbailey 0047a65
refactor: Drop the unused ceiling parameter from the delay validator
jsonbailey 30bb8ec
fix: Measure the start_wait tests against a monotonic clock and a bound
jsonbailey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,10 +10,15 @@ | |
| from ldclient.async_config import AsyncConfig | ||
| from ldclient.impl.aio.concurrency import AsyncEvent, AsyncRepeatingTask | ||
| from ldclient.impl.datasource.datasource_common import sink_or_store | ||
| from ldclient.impl.retry import ( | ||
| FailureKind, | ||
| RetryState, | ||
| classify_http_status, | ||
| for_polling | ||
| ) | ||
| from ldclient.impl.util import ( | ||
| UnsuccessfulResponseException, | ||
| http_error_message, | ||
| is_http_error_recoverable, | ||
| http_error_description, | ||
| log | ||
| ) | ||
| from ldclient.interfaces import ( | ||
|
|
@@ -27,13 +32,22 @@ | |
|
|
||
|
|
||
| class AsyncPollingUpdateProcessor(AsyncUpdateProcessor): | ||
| def __init__(self, config: AsyncConfig, requester: AsyncFeatureRequester, store: AsyncFeatureStore, ready: AsyncEvent): | ||
| """Polls LaunchDarkly for flag data on its own background task. | ||
|
|
||
| The loop reads its wait from the retry state, which ``_fetch_and_store`` | ||
| updates, so a failure can push the next poll further out than the poll | ||
| interval. See :mod:`ldclient.impl.retry`. | ||
| """ | ||
|
|
||
| def __init__(self, config: AsyncConfig, requester: AsyncFeatureRequester, store: AsyncFeatureStore, ready: AsyncEvent, retry_state: Optional[RetryState] = None): | ||
| self._config = config | ||
| self._data_source_update_sink = config.data_source_update_sink | ||
| self._requester = requester | ||
| self._store = store | ||
| self._ready = ready | ||
| self._task = AsyncRepeatingTask.at_interval("ldclient.datasource.polling", config.poll_interval, 0, self._fetch_and_store) | ||
| self._retry = retry_state or for_polling(config.poll_interval) | ||
| # No initial delay: the first poll is immediate. | ||
| self._task = AsyncRepeatingTask("ldclient.datasource.polling", self._retry, 0, self._fetch_and_store) | ||
|
|
||
| def start(self): | ||
| log.info("Starting AsyncPollingUpdateProcessor with request interval: " + str(self._config.poll_interval)) | ||
|
|
@@ -43,7 +57,12 @@ def initialized(self): | |
| return self._ready.is_set() and self._store.initialized | ||
|
|
||
| async def stop(self): | ||
| self.__stop_with_error_info(None) | ||
| log.info("Stopping AsyncPollingUpdateProcessor") | ||
| self._task.stop() | ||
|
|
||
| if self._data_source_update_sink is not None: | ||
| self._data_source_update_sink.update_status(DataSourceState.OFF, None) | ||
|
|
||
| # Wait for the current poll to finish before closing the transport, so we do | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Off is reported, but then the comment |
||
| # not close it while a request is still using it. The close is in a finally | ||
| # so an owned transport is still released if stop() is cancelled mid-wait. | ||
|
|
@@ -52,39 +71,42 @@ async def stop(self): | |
| finally: | ||
| await self._requester.close() | ||
|
|
||
| def __stop_with_error_info(self, error: Optional[DataSourceErrorInfo]): | ||
| log.info("Stopping AsyncPollingUpdateProcessor") | ||
| self._task.stop() | ||
|
|
||
| if self._data_source_update_sink is None: | ||
| return | ||
|
|
||
| self._data_source_update_sink.update_status(DataSourceState.OFF, error) | ||
|
|
||
| async def _fetch_and_store(self): | ||
| async def _fetch_and_store(self) -> None: | ||
| """Makes one poll request and records the outcome on the retry state.""" | ||
| try: | ||
| all_data = await self._requester.get_all_data() | ||
| await sink_or_store(self._data_source_update_sink, self._store).init(all_data) | ||
|
|
||
| if self._data_source_update_sink is not None: | ||
| self._data_source_update_sink.update_status(DataSourceState.VALID, None) | ||
|
|
||
| # Report the status before signaling readiness, so a caller that | ||
| # wakes on readiness cannot still read INITIALIZING. | ||
| if not self._ready.is_set() and self._store.initialized: | ||
| log.info("AsyncPollingUpdateProcessor initialized ok") | ||
| self._ready.set() | ||
|
|
||
| if self._data_source_update_sink is not None: | ||
| self._data_source_update_sink.update_status(DataSourceState.VALID, None) | ||
| self._retry.record_success() | ||
| return | ||
| except UnsuccessfulResponseException as e: | ||
| kind = classify_http_status(e.status) | ||
| error_info = DataSourceErrorInfo(DataSourceErrorKind.ERROR_RESPONSE, e.status, time.time(), str(e)) | ||
| description = "Received %s for polling request" % http_error_description(e.status) | ||
| level = log.error if kind is FailureKind.UNEXPECTED else log.warning | ||
| stacktrace = None | ||
| except Exception as e: | ||
| # A certificate failure lands here too, and is as normal as the rest. | ||
| kind = FailureKind.NORMAL | ||
| error_info = DataSourceErrorInfo(DataSourceErrorKind.UNKNOWN, 0, time.time(), str(e)) | ||
| description = "Error encountered when updating flags: %s" % e | ||
| level = log.error | ||
| # The exception is passed explicitly: by the time the message is | ||
| # logged, the handler has exited and exc_info() is empty. | ||
| stacktrace = e | ||
|
|
||
| http_error_message_result = http_error_message(e.status, "polling request") | ||
| if not is_http_error_recoverable(e.status): | ||
| log.error(http_error_message_result) | ||
| self._ready.set() # if client is initializing, make it stop waiting; has no effect if already inited | ||
| self.__stop_with_error_info(error_info) | ||
| else: | ||
| log.warning(http_error_message_result) | ||
| self._retry.record_failure(kind) | ||
| delay = self._retry.next_delay | ||
| level("%s - will retry in %.1fs" % (description, delay), exc_info=stacktrace) | ||
|
|
||
| if self._data_source_update_sink is not None: | ||
| self._data_source_update_sink.update_status(DataSourceState.INTERRUPTED, error_info) | ||
| except Exception as e: | ||
| log.exception('Error: Exception encountered when updating flags. %s' % e) | ||
| if self._data_source_update_sink is not None: | ||
| self._data_source_update_sink.update_status(DataSourceState.INTERRUPTED, DataSourceErrorInfo(DataSourceErrorKind.UNKNOWN, 0, time.time(), str(e))) | ||
| if self._data_source_update_sink is not None: | ||
| self._data_source_update_sink.update_status(DataSourceState.INTERRUPTED, error_info) | ||
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is
RetryDelayStrategy()an unused placeholder in this case?