Skip to content
Source
Sign inSign up
Open History to see the last commit

index.ts

130 lines5.0 KB

1import { authorize, githubKeys, type WriteIdentity } from './auth.ts';
2import { cborResponse } from './cbor.ts';
3import { cleanup, getScope, selectBlob } from './database.ts';
4import { errorResponse, HttpError, unavailable } from './errors.ts';
5import { fetchMetadata } from './fetch.ts';
6import { limitsFrom } from './limits.ts';
7import { store } from './store.ts';
8import { Deadline } from './streams.ts';
9import { Admission } from './admission.ts';
10import { Observations } from './observations.ts';
11​
12const keys = githubKeys();
13const admission = new Admission();
14​
15export default {
16 async fetch(request, env, ctx) {
17 const started = Date.now();
18 const id = crypto.randomUUID();
19 const stats = new Observations();
20 let scopeId = 'unknown';
21 let operation = 'unknown';
22 let outcome = 'error';
23 let identity: WriteIdentity | undefined;
24 let deadline: Deadline | undefined;
25 let release: (() => void) | undefined;
26 let response: Response;
27 try {
28 const limits = limitsFrom(env.LIMITS);
29 const url = new URL(request.url);
30 deadline = new Deadline(
31 url.pathname.endsWith('/store') ? limits.deadlineMs : Math.min(limits.deadlineMs, 15000),
32 request.signal,
33 );
34 const route =
35 /^\/projects\/([a-z0-9][a-z0-9-]{0,62})\/(fetch|store|blob\/([0-9a-f-]{36}))$/.exec(
36 url.pathname,
37 );
38 const namespaces: unknown = JSON.parse(env.NAMESPACES);
39 if (
40 !Array.isArray(namespaces) ||
41 namespaces.length > 100 ||
42 namespaces.some((value) => typeof value !== 'string')
43 )
44 throw new Error('Invalid namespaces');
45 const known = route && namespaces.includes(route[1]);
46 if (known) {
47 scopeId = route[1]!;
48 operation = route[2]!.startsWith('blob/') ? 'blob' : route[2]!;
49 }
50 const limiter =
51 operation === 'store' || url.pathname.endsWith('/store')
52 ? env.STORE_LIMITER
53 : env.READ_LIMITER;
54 const rateLimit = await deadline.run(
55 limiter.limit({ key: known ? `${scopeId}:${operation}` : 'unknown' }),
56 );
57 if (!rateLimit.success) throw new HttpError(429, 'rate_limit');
58 if (!known || url.search || request.method !== (operation === 'blob' ? 'GET' : 'POST'))
59 throw new HttpError(404, 'unknown_route');
60 release = admission.acquire(operation === 'store');
61 const scope = await deadline.run(getScope(env.INDEX, scopeId, stats));
62 if (operation === 'store') {
63 identity = await deadline.run(authorize(request, scope, keys));
64 response = await store(request, env, ctx, scope, identity, limits, deadline, stats);
65 outcome = 'stored';
66 } else if (operation === 'fetch') {
67 const result = await fetchMetadata(request, env, scopeId, limits, deadline, stats);
68 response = cborResponse(result);
69 outcome = result.kind;
70 } else {
71 const selected = await deadline.run(selectBlob(env.INDEX, scopeId, route[3]!, stats));
72 if (!selected) throw new HttpError(404, 'blob_missing');
73 stats.r2_operations++;
74 const object = await deadline
75 .run(env.ARTIFACTS.get(selected.blob_object))
76 .catch(() => unavailable());
77 if (!object) throw new HttpError(404, 'blob_missing');
78 if (object.size !== selected.blob_size) unavailable();
79 response = new Response(object.body, {
80 headers: {
81 'Content-Type': 'application/octet-stream',
82 'Content-Length': String(object.size),
83 'Cache-Control': 'no-store',
84 },
85 });
86 outcome = 'blob';
87 }
88 } catch (error) {
89 response = errorResponse(error);
90 if (error instanceof HttpError) outcome = error.code;
91 } finally {
92 deadline?.dispose();
93 release?.();
94 }
95 response.headers.set('X-Request-Id', id);
96 response.headers.set('X-Remote-Cache-Deployment', env.DEPLOYMENT_ID ?? 'local');
97 const sample = Math.min(1, Math.max(0, Number(env.LOG_SAMPLE_RATE) || 0));
98 // Fixed sampling bounds error volume as well as successful request volume.
99 if (Math.random() < sample)
100 console.log(
101 JSON.stringify({
102 request_id: id,
103 scope: scopeId,
104 operation,
105 status: response.status,
106 outcome,
107 ...(response.status >= 400 ? { error_class: outcome } : {}),
108 response_bytes: Number(response.headers.get('Content-Length') ?? 0),
109 duration_ms: Date.now() - started,
110 ...stats.toJSON(),
111 ...(stats.d1_storage_bytes >= 400000000 ? { warning: 'd1_storage_400mb' } : {}),
112 ...(identity
113 ? {
114 repository_id: identity.repository_id,
115 workflow_ref: identity.workflow_ref,
116 run_id: identity.run_id,
117 run_attempt: identity.run_attempt,
118 sha: identity.sha,
119 event_name: identity.event_name,
120 head_ref: identity.head_ref,
121 }
122 : {}),
123 }),
124 );
125 return response;
126 },
127 async scheduled(_event, env) {
128 await cleanup(env);
129 },
130} satisfies ExportedHandler<Env>;