Skip to content

feat: get plan bulk with cache#59

Open
tomerqodo wants to merge 4 commits intocopilot_combined_20260121_qodo_grep_cursor_copilot_1_base_feat_get_plan_bulk_with_cache_pr445from
copilot_combined_20260121_qodo_grep_cursor_copilot_1_head_feat_get_plan_bulk_with_cache_pr445
Open

feat: get plan bulk with cache#59
tomerqodo wants to merge 4 commits intocopilot_combined_20260121_qodo_grep_cursor_copilot_1_base_feat_get_plan_bulk_with_cache_pr445from
copilot_combined_20260121_qodo_grep_cursor_copilot_1_head_feat_get_plan_bulk_with_cache_pr445

Conversation

@tomerqodo
Copy link
Copy Markdown

Benchmark PR from qodo-benchmark#445

Copilot AI review requested due to automatic review settings January 21, 2026 18:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a caching layer for bulk billing plan retrieval to reduce load on the billing API during batch operations. It introduces get_plan_bulk_with_cache() that uses Redis to cache subscription plans with a 10-minute TTL, falling back to the API for cache misses.

Changes:

  • Added Redis-backed caching functionality for bulk plan retrieval with graceful fallback behavior
  • Enhanced error handling in get_plan_bulk to skip invalid tenant plans instead of failing entirely
  • Added comprehensive integration tests using testcontainers to verify cache behavior, TTL handling, and error recovery

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
api/services/billing_service.py Implements get_plan_bulk_with_cache() method with Redis caching, adds cache key generation helper, improves error handling in get_plan_bulk, and adds __str__ method to SubscriptionPlan
api/tests/unit_tests/services/test_billing_service.py Adds unit test verifying that invalid tenant plans are skipped during bulk retrieval
api/tests/test_containers_integration_tests/services/test_billing_service.py Adds comprehensive integration tests covering cache hit/miss scenarios, Redis failures, invalid data handling, and TTL expiration

if plans_to_cache:
try:
pipe = redis_client.pipeline()
for tenant_id, subscription_plan in bulk_plans.items():
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The iteration should use plans_to_cache.items() instead of bulk_plans.items() to ensure consistency with the dictionary being checked for caching. Although both dictionaries contain the same data in this context, using the same variable that was checked improves code clarity and maintainability.

Suggested change
for tenant_id, subscription_plan in bulk_plans.items():
for tenant_id, subscription_plan in plans_to_cache.items():

Copilot uses AI. Check for mistakes.
Comment on lines +1316 to +1319
assert len(result) == 2
assert "tenant-valid-1" in result
assert "tenant-valid-2" in result
assert "tenant-invalid" not in result
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The test expects invalid tenants to be excluded from the result (asserting 'tenant-invalid' not in result), but the implementation at line 292 sets results[tenant_id] = None for invalid plans. This means the test will fail because 'tenant-invalid' will be present in the result dictionary with a None value. The implementation should either skip adding invalid entries entirely (remove line 292) or the test expectations need to be updated.

Copilot uses AI. Check for mistakes.
Comment on lines +329 to +335
# Wait for TTL to expire (key will be deleted by Redis)
import time

time.sleep(2)

# Verify cache is expired (key doesn't exist)
cache_key_1 = BillingService._make_plan_cache_key("tenant-1")
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

Using time.sleep(2) in tests makes them slow and fragile. Consider using a shorter TTL or mocking the time/Redis behavior instead. This 2-second sleep will make the test suite noticeably slower, especially if run frequently in CI/CD pipelines.

Suggested change
# Wait for TTL to expire (key will be deleted by Redis)
import time
time.sleep(2)
# Verify cache is expired (key doesn't exist)
cache_key_1 = BillingService._make_plan_cache_key("tenant-1")
# Simulate TTL expiration by explicitly deleting the key from Redis
cache_key_1 = BillingService._make_plan_cache_key("tenant-1")
redis_client.delete(cache_key_1)
# Verify cache is expired (key doesn't exist)

Copilot uses AI. Check for mistakes.
Comment on lines +365 to +377
for tenant_id, subscription_plan in bulk_plans.items():
tenant_plans[tenant_id] = subscription_plan
plans_to_cache[tenant_id] = subscription_plan

# Step 3: Batch update Redis cache using pipeline
if plans_to_cache:
try:
pipe = redis_client.pipeline()
for tenant_id, subscription_plan in bulk_plans.items():
redis_key = cls._make_plan_cache_key(tenant_id)
# Serialize dict to JSON string
json_str = json.dumps(subscription_plan)
pipe.setex(redis_key, cls._PLAN_CACHE_TTL, json_str)
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The caching logic assumes all values in bulk_plans are valid SubscriptionPlan dictionaries, but get_plan_bulk can return None values for invalid tenants (line 292). When iterating over bulk_plans.items(), if a subscription_plan is None, the json.dumps(subscription_plan) call will serialize it as "null", and subsequent cache reads will fail validation. Consider filtering out None values from bulk_plans before caching, or ensuring get_plan_bulk never returns None values.

Copilot uses AI. Check for mistakes.
"""
Bulk fetch billing subscription plan with cache to reduce billing API loads in batch job scenarios.

NOTE: if you want to high data consistency, use get_plan_bulk instead.
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The documentation comment contains a grammatical error: "if you want to high data consistency" should be "if you want high data consistency" or "if you want to ensure high data consistency".

Suggested change
NOTE: if you want to high data consistency, use get_plan_bulk instead.
NOTE: if you want high data consistency, use get_plan_bulk instead.

Copilot uses AI. Check for mistakes.
logger.exception(
"get_plan_bulk: failed to validate subscription plan for tenant(%s)", tenant_id
)
results[tenant_id] = None
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

Setting results[tenant_id] = None for invalid tenant plans can cause issues downstream. The return type annotation is dict[str, SubscriptionPlan], but this stores None values instead of excluding invalid entries. Consider removing the invalid tenant from results entirely or filtering out None values before returning, consistent with the new caching behavior where invalid entries are skipped.

Suggested change
results[tenant_id] = None
# Skip invalid subscription plans instead of storing None
continue

Copilot uses AI. Check for mistakes.
Comment on lines +28 to 32
def __str__(self) -> str:
"""Return a human-readable string representation for debugging."""
return f"Plan: {self['plan']}, Expiration: {self['expiration_date']}"


Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The __str__ method is defined in a TypedDict class, which is not the correct place for instance methods. TypedDict classes are purely type annotations and do not support custom methods at runtime. This method will not be callable and should be removed or moved to a separate utility function.

Suggested change
def __str__(self) -> str:
"""Return a human-readable string representation for debugging."""
return f"Plan: {self['plan']}, Expiration: {self['expiration_date']}"
def format_subscription_plan(subscription: SubscriptionPlan) -> str:
"""Return a human-readable string representation for a SubscriptionPlan for debugging."""
return f"Plan: {subscription['plan']}, Expiration: {subscription['expiration_date']}"

Copilot uses AI. Check for mistakes.
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.

4 participants