Skip to content

Commit e2ca7f8

Browse files
committed
implement schema garbage collector endpoint
1 parent 13cdae8 commit e2ca7f8

25 files changed

+280
-85
lines changed

README.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ POST - `/schema/push` Creates a new graph and schema for a service.
3939
<summary>Example Request</summary>
4040
<p>
4141

42-
```json
42+
```jsonc
4343
{
4444
"type_defs": "type Query { hello: String }",
4545
"version": "1",
@@ -57,10 +57,10 @@ POST - `/schema/compose` Returns the last registered schema definition of all se
5757
<summary>Example Request</summary>
5858
<p>
5959

60-
```json
60+
```jsonc
6161
{
6262
"graph_name": "my_graph",
63-
"services": [{ "name": "foo", "version": "1" }]
63+
"services": [{ "name": "foo", "version": "1" }] // if versions can't be found it fails
6464
}
6565
```
6666

@@ -73,7 +73,7 @@ PUT - `/schema/deactivate` Deactivates a schema by id. The schema will no longer
7373
<summary>Example Request</summary>
7474
<p>
7575

76-
```json
76+
```jsonc
7777
{
7878
"graph_name": "my_graph",
7979
"schemaId": "916348424"
@@ -83,6 +83,21 @@ PUT - `/schema/deactivate` Deactivates a schema by id. The schema will no longer
8383
</p>
8484
</details>
8585

86+
POST - `/schema/garbage_collect` Removes all schemas except the most recent N of every service. Returns the removed schemas. This could be called by a [trigger](https://developers.cloudflare.com/workers/platform/cron-triggers).
87+
88+
<details>
89+
<summary>Example Request</summary>
90+
<p>
91+
92+
```jsonc
93+
{
94+
"num_schemas_keep": 10 // minimum is 10
95+
}
96+
```
97+
98+
</p>
99+
</details>
100+
86101
### Validation
87102

88103
POST - `/schema/diff` Returns the schema report of all services and the provided new schema.
@@ -147,9 +162,9 @@ DELETE - `/persisted_query` Deletes persisted query from KV Storage.
147162
<summary>Example Request</summary>
148163
<p>
149164

150-
```json
165+
```jsonc
151166
{
152-
"key": "apq:foo"
167+
"key": "foo"
153168
}
154169
```
155170

@@ -185,6 +200,7 @@ npm run dev
185200
### Benchmark
186201

187202
Run a benchmark with:
203+
188204
```
189205
docker run -e SECRET=<basic_auth_secret> -e URL=<worker_url> -i loadimpact/k6 run - < benchmark/composed-schema.js
190206
```

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { deletePersistedQuery } from './routes/delete-persisted-query'
1212
import { healthcheck } from './routes/healthcheck'
1313
import { deactivateSchema } from './routes/deactivate-schema'
1414
import { getGraphs } from './routes/get-graphs'
15+
import { garbageCollectSchemas } from './routes/garbage-collect'
1516

1617
const API = new Router()
1718

@@ -27,6 +28,11 @@ API.add(
2728
'/schema/compose',
2829
compose(basicAuth, getComposedSchemaByVersions),
2930
)
31+
API.add(
32+
'POST',
33+
'/schema/garbage_collect',
34+
compose(basicAuth, garbageCollectSchemas),
35+
)
3036

3137
// Tooling
3238
API.add('POST', '/schema/validate', compose(basicAuth, getSchemaValidation))

src/repositories/Graph.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ export interface Graph {
1111
created_at: number
1212
}
1313

14-
export interface GraphIndex {
15-
name: string
16-
}
17-
1814
export type NewGraph = Omit<Graph, 'created_at' | 'updated_at' | 'uid'>
1915

2016
export const key_owner = () => `graphs`
@@ -25,19 +21,19 @@ export function find(name: string) {
2521
return DB.read<Graph>(GRAPHS, key, 'json')
2622
}
2723

28-
export async function list(): Promise<GraphIndex[]> {
24+
export async function list(): Promise<Graph['name'][]> {
2925
const key = key_owner()
30-
return (await DB.read<GraphIndex[]>(GRAPHS, key, 'json')) || []
26+
return (await DB.read<Graph['name'][]>(GRAPHS, key, 'json')) || []
3127
}
3228

33-
export function syncIndex(versions: GraphIndex[]) {
29+
export function syncIndex(versions: string[]) {
3430
const key = key_owner()
3531
return DB.write(GRAPHS, key, versions)
3632
}
3733

3834
export function remove(name: string) {
3935
const key = key_item(name)
40-
return DB.read<Graph>(GRAPHS, key, 'json')
36+
return DB.remove(GRAPHS, key)
4137
}
4238

4339
export function save(item: Graph) {
@@ -57,9 +53,7 @@ export async function insert(graph: NewGraph) {
5753
return false
5854
}
5955

60-
let allGraphs = (await list()).concat({
61-
name: values.name,
62-
})
56+
let allGraphs = (await list()).concat(values.name)
6357

6458
if (!(await syncIndex(allGraphs))) {
6559
return false

src/repositories/Schema.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ declare const SCHEMAS: KV.Namespace
1010
export interface Schema {
1111
uid: string
1212
graph_name: string
13-
service_id: string
13+
service_name: string
1414
is_active: boolean
1515
hash: string
1616
type_defs: string
@@ -20,7 +20,7 @@ export interface Schema {
2020

2121
export interface SchemaIndex {
2222
uid: string
23-
service_id: string
23+
service_name: string
2424
graph_name: string
2525
hash: string
2626
}
@@ -57,7 +57,7 @@ export function syncIndex(graph_name: string, versions: SchemaIndex[]) {
5757

5858
export function remove(graph_name: string, uid: string) {
5959
const key = key_item(graph_name, uid)
60-
return DB.read<Schema>(SCHEMAS, key, 'json')
60+
return DB.remove(SCHEMAS, key)
6161
}
6262

6363
export function save(item: Schema) {
@@ -70,7 +70,7 @@ export async function insert(schema: NewSchema) {
7070
uid: ulid(),
7171
graph_name: schema.graph_name,
7272
hash: fnv1a(schema.type_defs).toString(),
73-
service_id: schema.service_id,
73+
service_name: schema.service_name,
7474
is_active: schema.is_active,
7575
type_defs: schema.type_defs,
7676
created_at: Date.now(),
@@ -83,7 +83,7 @@ export async function insert(schema: NewSchema) {
8383

8484
let allSchemas = (await list(schema.graph_name)).concat({
8585
uid: values.uid,
86-
service_id: values.service_id,
86+
service_name: values.service_name,
8787
graph_name: values.graph_name,
8888
hash: values.hash,
8989
})

src/repositories/SchemaVersion.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ export function find(graphName: string, serviceName: string, version: string) {
5252
return DB.read<SchemaVersion>(VERSIONS, key, 'json')
5353
}
5454

55+
export function remove(
56+
graphName: string,
57+
serviceName: string,
58+
version: string,
59+
) {
60+
const key = key_item(graphName, serviceName, version)
61+
return DB.remove(VERSIONS, key)
62+
}
63+
5564
export function save(
5665
graphName: string,
5766
serviceName: string,

src/routes/add-persisted-query.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import test from 'ava'
22
import { addPersistedQuery } from './add-persisted-query'
3-
import { NewNamespace, Request, Response } from '../test-utils'
3+
import { NewKVNamespace, Request, Response } from '../test-utils'
44

55
test.serial('Should store PQ from KV', async (t) => {
6-
const store = NewNamespace({
6+
const store = NewKVNamespace({
77
name: 'PERSISTED_QUERIES',
88
})
99

@@ -19,7 +19,7 @@ test.serial('Should store PQ from KV', async (t) => {
1919
test.serial(
2020
'Should return validation error because no query was provided',
2121
async (t) => {
22-
const store = NewNamespace({
22+
const store = NewKVNamespace({
2323
name: 'PERSISTED_QUERIES',
2424
})
2525

@@ -36,7 +36,7 @@ test.serial(
3636
},
3737
)
3838
test.serial('Should accept ttl values', async (t) => {
39-
const store = NewNamespace({
39+
const store = NewKVNamespace({
4040
name: 'PERSISTED_QUERIES',
4141
})
4242

src/routes/deactivate-schema.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import test from 'ava'
2-
import { createEmptyNamespaces, Request, Response } from '../test-utils'
2+
import { createEmptyKVNamespaces, Request, Response } from '../test-utils'
33
import { SchemaResponseModel, SuccessResponse } from '../types'
44
import { deactivateSchema } from './deactivate-schema'
55
import { getComposedSchema } from './get-composed-schema'
66
import { registerSchema } from './register-schema'
77

88
test.serial('Should deactivate schema', async (t) => {
9-
createEmptyNamespaces(['GRAPHS', 'SERVICES', 'SCHEMAS', 'VERSIONS'])
9+
createEmptyKVNamespaces(['GRAPHS', 'SERVICES', 'SCHEMAS', 'VERSIONS'])
1010

1111
let req = Request('POST', '', {
1212
type_defs: 'type Query { hello: String }',

src/routes/delete-persisted-query.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import test from 'ava'
2-
import { NewNamespace, Request, Response } from '../test-utils'
2+
import { NewKVNamespace, Request, Response } from '../test-utils'
33
import { addPersistedQuery } from './add-persisted-query'
44
import { getPersistedQuery } from './get-persisted-query'
55
import { deletePersistedQuery } from './delete-persisted-query'
66

77
test.serial('Should delete PQ from KV', async (t) => {
8-
NewNamespace({
8+
NewKVNamespace({
99
name: 'PERSISTED_QUERIES',
1010
})
1111

@@ -35,7 +35,7 @@ test.serial('Should delete PQ from KV', async (t) => {
3535
})
3636

3737
test.serial('Should return 400 when key was not provided', async (t) => {
38-
NewNamespace({
38+
NewKVNamespace({
3939
name: 'PERSISTED_QUERIES',
4040
})
4141

src/routes/garbage-collect.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import test from 'ava'
2+
import { createEmptyKVNamespaces, Request, Response } from '../test-utils'
3+
import {
4+
GarbageCollectResponseModel,
5+
SchemaResponseModel,
6+
SuccessResponse,
7+
} from '../types'
8+
import { garbageCollectSchemas } from './garbage-collect'
9+
import { getComposedSchema } from './get-composed-schema'
10+
import { registerSchema } from './register-schema'
11+
12+
test.serial(
13+
'Should keep the most recent 10 schemas of every servcie in the graph',
14+
async (t) => {
15+
createEmptyKVNamespaces(['GRAPHS', 'SERVICES', 'SCHEMAS', 'VERSIONS'])
16+
17+
for (let i = 0; i < 15; i++) {
18+
let req = Request('POST', '', {
19+
type_defs: `type Query { hello${i}: String }`,
20+
version: i.toString(),
21+
service_name: `foo`,
22+
graph_name: 'my_graph',
23+
})
24+
let res = Response()
25+
await registerSchema(req, res)
26+
t.is(res.statusCode, 200)
27+
28+
req = Request('POST', '', {
29+
type_defs: `type Query { world${i}: String }`,
30+
version: i.toString(),
31+
service_name: `bar`,
32+
graph_name: 'my_graph',
33+
})
34+
res = Response()
35+
await registerSchema(req, res)
36+
t.is(res.statusCode, 200)
37+
}
38+
39+
let req = Request('POST', '', {
40+
num_schemas_keep: 10,
41+
})
42+
let res = Response()
43+
await garbageCollectSchemas(req, res)
44+
45+
t.is(res.statusCode, 200)
46+
47+
const result = (res.body as any) as SuccessResponse<
48+
GarbageCollectResponseModel[]
49+
>
50+
51+
t.is(result.success, true)
52+
t.is(result.data.length, 10) // removed 5 schemas per service
53+
54+
t.truthy(result.data[0].schemaId)
55+
t.is(result.data[0].service_name, 'foo')
56+
t.is(result.data[0].graph_name, 'my_graph')
57+
58+
t.truthy(result.data[6].schemaId)
59+
t.is(result.data[6].service_name, 'bar')
60+
t.is(result.data[6].graph_name, 'my_graph')
61+
62+
req = Request('GET', 'graph_name=my_graph')
63+
res = Response()
64+
await getComposedSchema(req, res)
65+
66+
t.is(res.statusCode, 200)
67+
68+
const composed = (res.body as any) as SuccessResponse<SchemaResponseModel[]>
69+
70+
t.is(composed.success, true)
71+
t.is(composed.data.length, 2)
72+
73+
t.is(composed.data[0].version, '14')
74+
t.is(composed.data[0].service_name, 'foo')
75+
76+
t.is(composed.data[1].version, '14')
77+
t.is(composed.data[1].service_name, 'bar')
78+
},
79+
)

0 commit comments

Comments
 (0)