Skip to content

feat: emit FETCH_COMPLETED after every feature fetch - #456

Merged
ppicom merged 8 commits into
Unleash:mainfrom
Elyahou:feat/fetch-completed-event
Oct 28, 2026
Merged

ppicom merged 8 commits into
Unleash:mainfrom
Elyahou:feat/fetch-completed-event

Conversation

@Elyahou

@Elyahou Elyahou commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Why the change

A failed or unchanged fetch emits no event today (only FETCHED, for a changed payload, and READY), so an application that wants to count fetch failures or track how old its definitions are has to patch requests itself; this adds a FETCH_COMPLETED event after every fetch, like the Java SDK's togglesFetched(ClientFeaturesResponse).

Special things to note

  • FETCHED is unchanged and still fires only for a changed payload, so existing callbacks see the same events plus the new one.
  • The event carries no HTTP status code: the transport turns the response into a FetchStatus that says what a listener should do about it.
  • The transports emit it as soon as a fetch ends, before the result is applied, so it arrives before that poll's FETCHED/READY. It covers polling only; the streaming connector doesn't fetch, so it doesn't emit it.

Change outline

class FetchStatus(Enum):
    CHANGED = "changed"                    # 200 with a body
    NOT_CHANGED = "not_changed"            # 304
    UNAUTHORIZED = "unauthorized"          # 401, 403: won't recover on its own
    RATE_LIMITED = "rate_limited"          # 429: back off
    UNAVAILABLE = "unavailable"            # 5xx after retries, timeout, no response: transient
    INVALID_RESPONSE = "invalid_response"  # empty 200, any other status

@dataclass
class UnleashFetchCompletedEvent(BaseEvent):   # event_type=UnleashEventType.FETCH_COMPLETED
    status: FetchStatus

Counting fetch failures and tracking when definitions were last confirmed, through the existing event_callback:

from UnleashClient import UnleashClient
from UnleashClient.events import BaseEvent, FetchStatus, UnleashFetchCompletedEvent


def on_event(event: BaseEvent) -> None:
    if not isinstance(event, UnleashFetchCompletedEvent):
        return
    if event.status in (FetchStatus.CHANGED, FetchStatus.NOT_CHANGED):
        # Unleash answered with the current definitions
        last_successful_fetch.set_to_current_time()
    else:
        fetch_failures.inc(status=event.status.value)


client = UnleashClient(url=UNLEASH_URL, app_name="my-app", event_callback=on_event)
client.initialize_client()
_Transport / _AsyncTransport.fetch_features(etag)
  _fetch_features(etag)        # the request as before, plus how it ended as a FetchStatus
+ _emit_fetch_completed(...)   # FETCH_COMPLETED with that status
  return result                # the connectors apply it as before

Tests cover each status on both transports (200, 304, 401, 403, 429, a 500 after retries, 503, an unexpected 202, an empty 200, no response, and on the async one a body that times out), a listener that raises, and both clients delivering the event to their event_callback.

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@sighphyre sighphyre left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this is good. Want to wait for some feedback from others before I merge though

Comment thread UnleashClient/events.py
@alexanto

Copy link
Copy Markdown
Contributor

I think the new event should also be documented in the readme

@sighphyre

Copy link
Copy Markdown
Member

I think the new event should also be documented in the readme

Fair, but we're halfway through migrating docs to here: https://docs.getunleash.io/sdks/python

I think let's leave it in this PR, I can open a PR for that with the docs team (and maybe even clean up the README here to be what we agreed on with Documentation before they realize I haven't done that and they come for me)

@ppicom
ppicom requested a balanced review from Copilot October 22, 2026 12:44
alexanto
alexanto previously approved these changes Oct 22, 2026
@github-project-automation github-project-automation Bot moved this from New to Approved PRs in Issues and PRs Oct 22, 2026
@alexanto
alexanto self-requested a review October 22, 2026 12:45
@alexanto
alexanto dismissed their stale review October 22, 2026 12:46

from my view this is a sane addition, but lets wait for what the others think

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.

🟡 Changes recommended

Cache application errors suppress completion events in both polling connectors, breaking the per-fetch notification guarantee.

2 open issues

Review effort: Balanced

Comment thread UnleashClient/connectors/_async_connector.py Outdated
Comment thread UnleashClient/connectors/polling_connector.py Outdated
Comment thread UnleashClient/_feature_store.py Outdated
@github-project-automation github-project-automation Bot moved this from Approved PRs to In Progress in Issues and PRs Oct 22, 2026
@Elyahou
Elyahou marked this pull request as ready for review October 22, 2026 15:24
Comment thread UnleashClient/_transport.py Outdated
@ppicom

ppicom commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

@Elyahou thanks for the work on this PR, really appreciated. I left a question about including the http code in the event itself. The rest of it is looking nice!

@Elyahou

Elyahou commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

@Elyahou thanks for the work on this PR, really appreciated. I left a question about including the http code in the event itself. The rest of it is looking nice!

Thanks ! I replied to your comment

@Elyahou
Elyahou requested a review from ppicom October 26, 2026 17:16
The event no longer carries an HTTP status code. Instead the status says
what a listener should do about it: UNAUTHORIZED (401/403, won't recover
on its own), RATE_LIMITED (429, back off), UNAVAILABLE (5xx after retries,
timeout or no response) and INVALID_RESPONSE (empty 200 or any other
status) replace FAILED. The code is read in one place, in the transport,
so the event stays free of HTTP.

The sync Retry goes back to raise_on_status: a 5xx after retries is
UNAVAILABLE whether or not the last response is handed back.
@Elyahou
Elyahou force-pushed the feat/fetch-completed-event branch from 9741d2e to 2b2e594 Compare October 27, 2026 15:46
@Elyahou
Elyahou requested a review from ppicom October 27, 2026 15:47
…d-event

# Conflicts:
#	CHANGELOG.md
#	UnleashClient/_async_transport.py
#	tests/unit_tests/test_async_transport.py
#	tests/unit_tests/test_transport.py

@ppicom ppicom 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.

Looks good, I think FAILED got renamed to UNAVAILABLE again. If we sort that, we can merge 🚀

@github-project-automation github-project-automation Bot moved this from In Progress to Approved PRs in Issues and PRs Oct 28, 2026
@ppicom
ppicom merged commit 1e62629 into Unleash:main Oct 28, 2026
8 checks passed
@github-project-automation github-project-automation Bot moved this from Approved PRs to Done in Issues and PRs Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants