Skip to content
This repository was archived by the owner on Aug 21, 2025. It is now read-only.

Commit ebcf063

Browse files
committed
Implement Cloudflare Queues for GitHub API rate limiting
- Updated README.md to include Cloudflare Queues for managing GitHub API requests. - Modified wrangler.toml to configure the GitHub task queue. - Enhanced handlers to send messages to the queue for fetching repository issues, contributors, and pull requests. - Added queue processing logic in index.ts to handle queued tasks. - Introduced new types in types.ts for GitHub task messages. These changes improve the application's ability to handle GitHub API interactions efficiently while respecting rate limits.
1 parent 1c53734 commit ebcf063

6 files changed

Lines changed: 335 additions & 35 deletions

File tree

README.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This project is a port of the Friends of Shopware API to Cloudflare Workers, usi
1010
- Packagist packages information
1111
- GitHub webhook for issue updates
1212
- Data cached in Cloudflare KV storage
13+
- Uses Cloudflare Queues for rate-limited GitHub API operations
1314

1415
## Setup
1516

@@ -19,14 +20,24 @@ This project is a port of the Friends of Shopware API to Cloudflare Workers, usi
1920
npm install
2021
```
2122

22-
2. Configure your KV namespace:
23+
2. Configure your KV namespace and Queue:
2324

24-
Create a KV namespace in your Cloudflare Workers dashboard and update the `wrangler.toml` file with your KV namespace IDs.
25+
Create a KV namespace and a Queue in your Cloudflare Workers dashboard and update the `wrangler.toml` file with your IDs.
2526

2627
```toml
2728
kv_namespaces = [
2829
{ binding = "STORAGE", id = "your-kv-namespace-id", preview_id = "your-preview-kv-namespace-id" }
2930
]
31+
32+
[[queues.producers]]
33+
queue = "github-tasks"
34+
binding = "GITHUB_QUEUE"
35+
36+
[[queues.consumers]]
37+
queue = "github-tasks"
38+
max_batch_size = 10
39+
max_batch_timeout = 30
40+
max_retries = 3
3041
```
3142

3243
3. Set your GitHub token:
@@ -63,11 +74,26 @@ npm run deploy
6374
- `GET /v2/packagist/packages` - List all packages
6475
- `POST /webhook/issue` - GitHub webhook for issue updates
6576

66-
## Scheduled Tasks
77+
## Queue Implementation
78+
79+
The project uses Cloudflare Queues to manage GitHub API requests. This helps to:
80+
81+
1. Respect GitHub API rate limits
82+
2. Make API interactions more resilient
83+
3. Process tasks asynchronously
84+
85+
Tasks are dispatched to the queues during cron jobs:
6786

6887
- Hourly: Refresh GitHub stats and Packagist stats
6988
- Every 5 minutes: Refresh repository issues
7089

90+
## Scheduled Tasks
91+
92+
The application uses Cloudflare Cron Triggers to perform these tasks:
93+
94+
- `0 * * * *` (Hourly): Refresh GitHub stats and Packagist stats
95+
- `*/5 * * * *` (Every 5 minutes): Refresh repository issues
96+
7197
## License
7298

7399
MIT

src/handlers.ts

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Context } from 'hono';
22
import { GitHubService } from './services/github';
33
import { PackagistService } from './services/packagist';
44
import { CacheService } from './services/cache';
5-
import { AppContext, Env, GithubWebhook } from './types';
5+
import { AppContext, Env, GithubWebhook, GitHubTaskMessage } from './types';
66

77
// Main Routes Handler
88
export const listRepositories = async (c: Context<{ Bindings: Env }>) => {
@@ -40,19 +40,16 @@ export const githubIssueWebhook = async (c: Context<{ Bindings: Env }>) => {
4040
const payload: GithubWebhook = await c.req.json();
4141
const repoName = payload.repository.name;
4242
const ownerLogin = payload.repository.owner.login;
43+
44+
// Send a message to the queue to update issues
45+
await c.env.GITHUB_QUEUE.send({
46+
type: 'fetch-repository-issues',
47+
owner: ownerLogin,
48+
repo: repoName,
49+
timestamp: Date.now()
50+
} as GitHubTaskMessage);
4351

44-
const appContext: AppContext = {
45-
env: c.env,
46-
octokit: c.env.octokit!
47-
};
48-
49-
const githubService = new GitHubService(appContext);
50-
const cacheService = new CacheService(c.env);
51-
52-
const issues = await githubService.getAllIssues(ownerLogin, repoName);
53-
await cacheService.updateIssuesForRepository(repoName, issues);
54-
55-
return c.text(`Updated issues for ${repoName}`, 200);
52+
return c.text(`Queued update for issues of ${repoName}`, 200);
5653
} catch (error) {
5754
console.error('Error processing webhook:', error);
5855
return c.text('Error processing webhook', 500);
@@ -61,7 +58,7 @@ export const githubIssueWebhook = async (c: Context<{ Bindings: Env }>) => {
6158

6259
// Background refreshers
6360
export const refreshGithubStats = async (context: AppContext) => {
64-
console.log('Refreshing Github Stats');
61+
console.log('Fetching repositories and queueing GitHub stats tasks');
6562
const githubService = new GitHubService(context);
6663
const cacheService = new CacheService(context.env);
6764

@@ -70,27 +67,58 @@ export const refreshGithubStats = async (context: AppContext) => {
7067
const repos = await githubService.getAllRepos(orgName);
7168
await cacheService.setRepositories(repos);
7269

73-
// Get and cache contributors
74-
const contributors = await githubService.getUserContributions(repos);
75-
await cacheService.setContributors(contributors);
70+
// Queue contributor and PR tasks for each repository
71+
for (const repo of repos) {
72+
// Queue contributors fetch
73+
await context.env.GITHUB_QUEUE.send({
74+
type: 'fetch-repository-contributors',
75+
owner: repo.owner.login,
76+
repo: repo.name,
77+
timestamp: Date.now()
78+
} as GitHubTaskMessage);
79+
80+
// Queue pull requests fetch
81+
await context.env.GITHUB_QUEUE.send({
82+
type: 'fetch-repository-pull-requests',
83+
owner: repo.owner.login,
84+
repo: repo.name,
85+
timestamp: Date.now()
86+
} as GitHubTaskMessage);
87+
}
7688

77-
console.log('Refreshed Github Stats');
89+
// Queue a task to process all contributor data after the individual tasks
90+
// We'll send this separately without a delay since Cloudflare Workers queues
91+
// will process messages in roughly the order they were received
92+
await context.env.GITHUB_QUEUE.send({
93+
type: 'process-contributors',
94+
owner: orgName,
95+
timestamp: Date.now(),
96+
metadata: {
97+
// Add a higher priority flag that we can check in the consumer
98+
highPriority: false
99+
}
100+
} as GitHubTaskMessage);
101+
102+
console.log('Queued GitHub stats tasks for all repositories');
78103
};
79104

80105
export const refreshRepositoryIssues = async (context: AppContext) => {
81-
console.log('Refreshing Repository Issues');
82-
const githubService = new GitHubService(context);
106+
console.log('Queueing Repository Issues refresh tasks');
83107
const cacheService = new CacheService(context.env);
84108

85109
const repos = await cacheService.getRepositories();
86-
const issues: Record<string, any[]> = {};
87110

111+
// Queue issue fetching for each repository
88112
for (const repo of repos) {
89-
issues[repo.name] = await githubService.getAllIssues(repo.owner.login, repo.name);
113+
await context.env.GITHUB_QUEUE.send({
114+
type: 'fetch-repository-issues',
115+
owner: repo.owner.login,
116+
repo: repo.name,
117+
timestamp: Date.now()
118+
} as GitHubTaskMessage);
90119
}
91120

92-
await cacheService.setIssues(issues);
93-
console.log('Refreshed Repository Issues');
121+
console.log('Queued Repository Issues tasks');
94122
};
95123

96124
export const refreshPackagistStats = async (context: AppContext) => {

src/index.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Hono } from 'hono';
22
import { cors } from 'hono/cors';
33
import { Octokit } from '@octokit/rest';
4-
import { Env, AppContext } from './types';
4+
import { Env, AppContext, GitHubTaskMessage, MessageBatch } from './types';
55
import {
66
listRepositories,
77
listContributors,
@@ -12,6 +12,7 @@ import {
1212
refreshRepositoryIssues,
1313
refreshPackagistStats
1414
} from './handlers';
15+
import { processGitHubTasks } from './queue';
1516

1617
// Define app with bindings
1718
const app = new Hono<{ Bindings: Env }>();
@@ -48,8 +49,16 @@ interface ScheduledController {
4849
export default {
4950
fetch: app.fetch,
5051

52+
// Queue handler - must be declared as a function, not an object
53+
async queue(batch: MessageBatch<GitHubTaskMessage>, env: Env, ctx: ExecutionContext) {
54+
// Get the queue name from the batch
55+
if (batch.queue === 'github-tasks') {
56+
await processGitHubTasks(batch, env);
57+
}
58+
},
59+
5160
// Handle scheduled events
52-
scheduled: async (controller: ScheduledController, env: Env, ctx: ExecutionContext) => {
61+
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
5362
// Create context for scheduled handlers
5463
const context: AppContext = {
5564
env,
@@ -60,13 +69,14 @@ export default {
6069

6170
// Determine which refresh operation to perform based on cron schedule
6271
if (controller.cron === '0 * * * *') {
63-
console.log('Refreshing Github Stats');
72+
console.log('Starting Github Stats hourly job');
6473
// Hourly jobs
65-
ctx.waitUntil(refreshGithubStats(context));
66-
ctx.waitUntil(refreshPackagistStats(context));
74+
await refreshGithubStats(context);
75+
await refreshPackagistStats(context);
6776
} else if (controller.cron === '*/5 * * * *') {
77+
console.log('Starting Repository Issues 5-minute job');
6878
// Every 5 minutes
69-
ctx.waitUntil(refreshRepositoryIssues(context));
79+
await refreshRepositoryIssues(context);
7080
}
7181
}
7282
};

0 commit comments

Comments
 (0)