Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ data:
connectorSubtype: api
connectorType: source
definitionId: ef69ef6e-aa7f-4af1-a01d-ef775033524e
dockerImageTag: 2.1.14
dockerImageTag: 2.1.15
dockerRepository: airbyte/source-github
documentationUrl: https://docs.airbyte.com/integrations/sources/github
erdUrl: https://dbdocs.io/airbyteio/source-github?view=relationships
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
version = "2.1.14"
version = "2.1.15"
name = "source-github"
description = "Source implementation for GitHub."
authors = [ "Airbyte <contact@airbyte.io>",]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,11 @@ def read_records(self, stream_slice: Mapping[str, Any] = None, **kwargs) -> Iter

self.logger.warning(error_msg)
except GitHubAPILimitException as e:
internal_message = (
f"Stream: `{self.name}`, slice: `{stream_slice}`. Limits for all provided tokens are reached, please try again later"
)
message = "Rate Limits for all provided tokens are reached. For more information please refer to documentation: https://docs.airbyte.com/integrations/sources/github#limitations--troubleshooting"
raise AirbyteTracedException(internal_message=internal_message, message=message, failure_type=FailureType.config_error) from e
internal_message = f"Stream: `{self.name}`, slice: `{stream_slice}`. {e}"
message = "Rate limit exceeded for all configured GitHub API tokens."
raise AirbyteTracedException(
internal_message=internal_message, message=message, failure_type=FailureType.transient_error
) from e


class GithubStream(GithubStreamABC):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,41 @@ def check_all_tokens(self):
for token in self._tokens:
self._check_token_limits(token)

# Maximum duration for a single sleep interval when waiting for rate limit reset.
# Kept short to allow the platform's heartbeat mechanism to detect the connector is still alive.
SLEEP_INTERVAL_SECONDS = 60

def process_token(self, current_token, count_attr, reset_attr):
if getattr(current_token, count_attr) > 0:
setattr(current_token, count_attr, getattr(current_token, count_attr) - 1)
return True
elif all(getattr(x, count_attr) == 0 for x in self._tokens.values()):
min_time_to_wait = min((getattr(x, reset_attr) - ab_datetime_now()).total_seconds() for x in self._tokens.values())
if min_time_to_wait < self.max_time:
time.sleep(min_time_to_wait if min_time_to_wait > 0 else 0)
wait_time = max(min_time_to_wait, 0)
self._sleep_with_heartbeat(wait_time, count_attr)
self.check_all_tokens()
else:
raise GitHubAPILimitException(f"Rate limits for all tokens ({count_attr}) were reached")
else:
self.update_token()
return False

def _sleep_with_heartbeat(self, total_wait: float, count_attr: str) -> None:
"""Sleep in small intervals, logging periodically so the platform heartbeat sees activity."""
remaining = total_wait
self._logger.info(
"Rate limit reached for all tokens (%s). Waiting %.0f seconds for reset.",
count_attr,
total_wait,
)
while remaining > 0:
interval = min(remaining, self.SLEEP_INTERVAL_SECONDS)
time.sleep(interval)
remaining -= interval
if remaining > 0:
self._logger.info(
"Still waiting for rate limit reset (%s). %.0f seconds remaining.",
count_attr,
remaining,
)
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,9 @@ def request_callback_orgs(request, context):
with pytest.raises(AirbyteTracedException) as e:
list(read_full_refresh(stream))
assert [(x.count_rest, x.count_graphql) for x in authenticator._tokens.values()] == [(0, 500), (0, 500), (0, 500)]
message = (
"Stream: `organizations`, slice: `{'organization': 'org1'}`. Limits for all provided tokens are reached, please try again later"
)
assert e.value.failure_type == FailureType.config_error
assert e.value.internal_message == message
assert e.value.failure_type == FailureType.transient_error
assert "Stream: `organizations`" in e.value.internal_message
assert "Rate limits for all tokens" in e.value.internal_message


@freeze_time("2021-01-01 12:00:00")
Expand Down Expand Up @@ -148,7 +146,10 @@ def request_callback_orgs(request, context):
)

list(read_full_refresh(stream))
sleep_mock.assert_called_once_with(ACCEPTED_WAITING_TIME_IN_SECONDS)
# Sleep is now called in intervals of SLEEP_INTERVAL_SECONDS (60s) instead of a single long sleep
assert sleep_mock.call_count >= 1
total_slept = sum(call.args[0] for call in sleep_mock.call_args_list)
assert abs(total_slept - ACCEPTED_WAITING_TIME_IN_SECONDS) < 1
assert [(x.count_rest, x.count_graphql) for x in authenticator._tokens.values()] == [(500, 500), (500, 500), (498, 500)]


Expand Down
1 change: 1 addition & 0 deletions docs/integrations/sources/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ Your token should have at least the `repo` scope. Depending on which streams you

| Version | Date | Pull Request | Subject |
|:-----------|:-----------|:------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 2.1.15 | 2026-03-12 | [TBD](https://github.com/airbytehq/airbyte/pull/TBD) | Fix rate limit sleep blocking heartbeat; classify rate limit errors as transient |
| 2.1.14 | 2026-03-09 | [74284](https://github.com/airbytehq/airbyte/pull/74284) | Fix heartbeat timeout for pull_request_stats by using descending sort on incremental syncs |
| 2.1.13 | 2026-03-03 | [73698](https://github.com/airbytehq/airbyte/pull/73698) | feat(source-github): use GraphQL API for Releases stream to bypass 10k REST limit |
| 2.1.12 | 2026-03-03 | [74204](https://github.com/airbytehq/airbyte/pull/74204) | Update dependencies |
Expand Down
Loading