Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
58 changes: 11 additions & 47 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,48 +1,12 @@
.venv
__pycache__/
*.py[cod]
*$py.class
.pytest_cache/
.env
__pycache__
.pytest_cache
.pypirc
*.db
test
test_state.json
task_flow.egg-info
example_repo
signature.js
git-filter-repo
task/orca/
**/dist/
# yarn.lock
package-lock.json
node_modules
build
migrate.sh
*/dev.js
executables/*
namespace/*
config/*
.env.local
taskStateInfoKeypair.json
localKOIIDB.db
metadata.json
.npmrc
*.pem
.vscode
.cursor
data/chunks
data/process
test_state.csv
todos-example.csv


# Ignore auto-generated repository directories
repos/


# Ignore Data
data/*


venv

**/venv/
.venv/
venv/
dist/
build/
*.egg-info/
.coverage
htmlcov/
61 changes: 61 additions & 0 deletions src/retry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import time
import random
from functools import wraps
from typing import Any, Callable, Optional, Tuple, Type

class RetryError(Exception):
"""Exception raised when all retry attempts are exhausted."""
pass

def retry(
max_attempts: int = 3,
backoff_base: float = 1.0,
backoff_multiplier: float = 2.0,
jitter: float = 0.1,
retryable_exceptions: Optional[Tuple[Type[Exception], ...]] = None
) -> Callable:
"""
A decorator that implements exponential backoff retry mechanism.

Args:
max_attempts (int): Maximum number of retry attempts. Defaults to 3.
backoff_base (float): Base time (in seconds) for initial wait. Defaults to 1.0.
backoff_multiplier (float): Factor to increase wait time between retries. Defaults to 2.0.
jitter (float): Random variation to prevent synchronized retries. Defaults to 0.1.
retryable_exceptions (tuple): Exceptions that trigger a retry. Defaults to None.

Returns:
Decorated function with retry capabilities.
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
attempts = 0
default_retryable_exceptions = (
ConnectionError,
TimeoutError,
RuntimeError
)

exceptions_to_retry = retryable_exceptions or default_retryable_exceptions

while attempts < max_attempts:
try:
return func(*args, **kwargs)

except exceptions_to_retry as e:
attempts += 1

# Exit if max attempts reached
if attempts >= max_attempts:
raise RetryError(f"Function {func.__name__} failed after {max_attempts} attempts") from e

# Calculate exponential backoff with jitter
wait_time = backoff_base * (backoff_multiplier ** attempts)
jittered_wait = wait_time * (1 + random.uniform(-jitter, jitter))

print(f"Retry {attempts}/{max_attempts} for {func.__name__}. Waiting {jittered_wait:.2f} seconds.")
time.sleep(jittered_wait)

return wrapper
return decorator
81 changes: 81 additions & 0 deletions tests/test_retry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import pytest
import time
from src.retry import retry, RetryError

def test_retry_successful_function():
@retry(max_attempts=3)
def always_succeeds():
return "Success"

result = always_succeeds()
assert result == "Success"

def test_retry_fails_after_max_attempts():
attempts = 0

@retry(max_attempts=3)
def always_fails():
nonlocal attempts
attempts += 1
raise ConnectionError("Simulated network error")

with pytest.raises(RetryError):
always_fails()

assert attempts == 3

def test_retry_eventually_succeeds():
attempts = 0

@retry(max_attempts=3)
def intermittent_success():
nonlocal attempts
attempts += 1
if attempts < 3:
raise ConnectionError("Temporary error")
return "Final Success"

result = intermittent_success()
assert result == "Final Success"
assert attempts == 3

def test_retry_custom_exception():
class CustomError(Exception):
pass

attempts = 0

@retry(max_attempts=3, retryable_exceptions=(CustomError,))
def custom_error_function():
nonlocal attempts
attempts += 1
raise CustomError("Custom error")

with pytest.raises(RetryError):
custom_error_function()

assert attempts == 3

def test_retry_backoff_timing():
import time

start_time = time.time()
attempts = 0

@retry(max_attempts=3, backoff_base=0.1, backoff_multiplier=2.0)
def slow_function():
nonlocal attempts
attempts += 1
if attempts < 3:
raise ConnectionError("Temporary network error")
return "Success"

result = slow_function()
end_time = time.time()

assert result == "Success"
assert attempts == 3

# Check if total wait time is reasonable (allow for some variance)
total_wait = end_time - start_time
assert total_wait >= 0.2 and total_wait <= 1.0 # More flexible timing