@toa.io/extensions.exposition 1.0.0-alpha.269 → 1.0.0-alpha.270
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/components/exposition.stash/manifest.toa.yaml +13 -4
- package/components/identity.passkeys/operations/tsconfig.tsbuildinfo +1 -1
- package/documentation/io.md +26 -23
- package/documentation/notes/throttling.md +39 -5
- package/features/etag.feature +55 -5
- package/features/io.throttle.feature +3 -5
- package/package.json +5 -5
- package/schemas/io/throttle.cos.yaml +2 -3
- package/source/Directive.test.ts +3 -0
- package/source/Directive.ts +25 -4
- package/source/Endpoint.ts +7 -11
- package/source/Factory.ts +16 -1
- package/source/HTTP/Context.ts +39 -8
- package/source/HTTP/Server.ts +16 -11
- package/source/HTTP/exceptions.ts +9 -3
- package/source/HTTP/messages.ts +31 -0
- package/source/RTD/Directives.ts +6 -0
- package/source/directives/auth/Authorization.ts +29 -3
- package/source/directives/io/Directive.ts +5 -4
- package/source/directives/io/IO.ts +15 -10
- package/source/directives/io/Input.ts +3 -3
- package/source/directives/io/Output.ts +12 -4
- package/source/directives/io/Throttle.ts +9 -12
- package/source/directives/io/lib/throttle/Configuration.test.ts +1 -2
- package/source/directives/io/lib/throttle/Configuration.ts +2 -4
- package/source/directives/io/lib/throttle/Quotas.test.ts +233 -148
- package/source/directives/io/lib/throttle/Quotas.ts +129 -53
- package/source/directives/io/lib/throttle/Sync.test.ts +153 -0
- package/source/directives/io/lib/throttle/Sync.ts +99 -0
- package/source/directives/io/lib/throttle/index.ts +1 -0
- package/source/directives/io/schemas.test.ts +7 -1
- package/transpiled/Directive.d.ts +2 -0
- package/transpiled/Directive.js +16 -4
- package/transpiled/Directive.js.map +1 -1
- package/transpiled/Endpoint.js +7 -8
- package/transpiled/Endpoint.js.map +1 -1
- package/transpiled/Factory.js +14 -1
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/HTTP/Context.d.ts +1 -1
- package/transpiled/HTTP/Context.js +30 -8
- package/transpiled/HTTP/Context.js.map +1 -1
- package/transpiled/HTTP/Server.js +14 -10
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/exceptions.d.ts +5 -2
- package/transpiled/HTTP/exceptions.js +7 -3
- package/transpiled/HTTP/exceptions.js.map +1 -1
- package/transpiled/HTTP/messages.d.ts +2 -0
- package/transpiled/HTTP/messages.js +21 -0
- package/transpiled/HTTP/messages.js.map +1 -1
- package/transpiled/RTD/Directives.d.ts +5 -0
- package/transpiled/directives/auth/Authorization.d.ts +1 -0
- package/transpiled/directives/auth/Authorization.js +21 -2
- package/transpiled/directives/auth/Authorization.js.map +1 -1
- package/transpiled/directives/io/Directive.d.ts +4 -4
- package/transpiled/directives/io/IO.d.ts +9 -4
- package/transpiled/directives/io/IO.js +14 -9
- package/transpiled/directives/io/IO.js.map +1 -1
- package/transpiled/directives/io/Input.d.ts +1 -1
- package/transpiled/directives/io/Input.js +3 -3
- package/transpiled/directives/io/Input.js.map +1 -1
- package/transpiled/directives/io/Output.d.ts +2 -0
- package/transpiled/directives/io/Output.js +9 -3
- package/transpiled/directives/io/Output.js.map +1 -1
- package/transpiled/directives/io/Throttle.d.ts +3 -5
- package/transpiled/directives/io/Throttle.js +8 -10
- package/transpiled/directives/io/Throttle.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Configuration.d.ts +0 -1
- package/transpiled/directives/io/lib/throttle/Configuration.js +2 -3
- package/transpiled/directives/io/lib/throttle/Configuration.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Quotas.d.ts +46 -22
- package/transpiled/directives/io/lib/throttle/Quotas.js +107 -46
- package/transpiled/directives/io/lib/throttle/Quotas.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Sync.d.ts +30 -0
- package/transpiled/directives/io/lib/throttle/Sync.js +76 -0
- package/transpiled/directives/io/lib/throttle/Sync.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/index.d.ts +1 -0
- package/transpiled/directives/io/lib/throttle/index.js +3 -1
- package/transpiled/directives/io/lib/throttle/index.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
- package/components/exposition.stash/operations/count.js +0 -7
|
@@ -1,107 +1,183 @@
|
|
|
1
|
-
import { console } from 'openspan'
|
|
2
1
|
import { Keys } from './Keys'
|
|
3
|
-
import type {
|
|
2
|
+
import type { Batch } from './Sync'
|
|
4
3
|
import type { Configuration } from './Configuration'
|
|
5
4
|
import type { Parameter } from '../../../../RTD'
|
|
6
5
|
import type { Input as Context, Output } from '../../../../io'
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
|
-
*
|
|
8
|
+
* A quota per key, metered by GCRA, decided in this process and shared with the others.
|
|
10
9
|
*
|
|
11
|
-
* The
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* The state of a key is one theoretical arrival time: the moment the key would be back
|
|
11
|
+
* at zero if nothing more arrived. An admitted request pushes it `emission` further out,
|
|
12
|
+
* time drags it back, and a request is admitted while it stays within `capacity` of now.
|
|
13
|
+
* So `requests` is what a key may spend at once, and `requests / interval` what it earns
|
|
14
|
+
* back — no window to burst across the edge of, and no lockout to time out of.
|
|
15
|
+
*
|
|
16
|
+
* Deciding needs nothing but the local map, which is what keeps the request path free of
|
|
17
|
+
* I/O. What the other gateways have spent arrives on {@link Sync}'s tick, as a debt in
|
|
18
|
+
* milliseconds — a duration, so it carries across processes without their clocks having
|
|
19
|
+
* to agree, and additive, which is what lets each process report only its own increments.
|
|
15
20
|
*/
|
|
16
21
|
export class Quotas {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
/** How often the debt is worth reconciling with the other gateways, in milliseconds. */
|
|
23
|
+
public readonly period: number
|
|
24
|
+
|
|
25
|
+
/** Milliseconds of debt an admitted request adds, `interval / requests`. */
|
|
26
|
+
private readonly emission: number
|
|
27
|
+
|
|
28
|
+
/** Debt a key may carry before it is refused, `interval` — a burst of `requests`. */
|
|
29
|
+
private readonly capacity: number
|
|
30
|
+
|
|
20
31
|
private readonly keys: Keys
|
|
21
|
-
private readonly
|
|
22
|
-
private readonly
|
|
23
|
-
private readonly
|
|
32
|
+
private readonly prefix: string
|
|
33
|
+
private readonly conditional: boolean
|
|
34
|
+
private readonly entries = new Map<string, Entry>()
|
|
24
35
|
|
|
25
36
|
/**
|
|
26
|
-
* The key computed at preflight, for settle to
|
|
37
|
+
* The key computed at preflight, for settle to charge against.
|
|
27
38
|
*
|
|
28
39
|
* `segment` reads the route parameters, which settle is not given, and `identity`
|
|
29
40
|
* can be refreshed between the two — so recomputing there would key a request
|
|
30
|
-
* differently than it was checked, and
|
|
41
|
+
* differently than it was checked, and throttling would quietly stop working.
|
|
31
42
|
*/
|
|
32
43
|
private readonly keyed = new WeakMap<Context, string>()
|
|
33
|
-
private remote: Remote | null = null
|
|
34
44
|
|
|
35
45
|
public constructor (options: Options) {
|
|
36
|
-
this.requests = options.requests
|
|
37
|
-
this.interval = options.interval
|
|
38
|
-
this.cooldown = options.cooldown
|
|
39
46
|
this.keys = options.keys
|
|
40
|
-
this.
|
|
47
|
+
this.conditional = options.conditional
|
|
48
|
+
this.emission = options.interval / options.requests
|
|
49
|
+
this.capacity = options.interval
|
|
50
|
+
this.period = period(options.interval)
|
|
51
|
+
|
|
52
|
+
// a key means one budget, so quotas that do not share one must not share a key
|
|
53
|
+
this.prefix = `t:${options.requests}:${options.interval}:`
|
|
41
54
|
}
|
|
42
55
|
|
|
43
|
-
public static create (configuration: Configuration,
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const keys = Keys.create(configuration.key, configuration.condition, route)
|
|
56
|
+
public static create (configuration: Configuration, route: string = ''): Quotas {
|
|
57
|
+
const { requests, interval, condition } = configuration
|
|
58
|
+
const keys = Keys.create(configuration.key, condition, route)
|
|
47
59
|
|
|
48
|
-
return new this({ requests, interval,
|
|
60
|
+
return new this({ keys, requests, interval, conditional: condition !== undefined })
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Seconds to wait before the request would be admitted, or zero when it is.
|
|
65
|
+
*
|
|
66
|
+
* Charges here when nothing conditions the count, so that the request is metered by
|
|
67
|
+
* the same call that admits it; otherwise only the response can tell whether it counts,
|
|
68
|
+
* and {@link use} charges once it can.
|
|
69
|
+
*/
|
|
70
|
+
public check (context: Context, parameters: Parameter[]): number {
|
|
52
71
|
const key = this.keys.get(context, parameters)
|
|
53
72
|
|
|
54
73
|
this.keyed.set(context, key)
|
|
55
74
|
|
|
56
|
-
|
|
75
|
+
const now = Date.now()
|
|
76
|
+
const entry = this.entries.get(key)
|
|
77
|
+
const tat = Math.max(entry?.tat ?? now, now)
|
|
78
|
+
const admit = tat + this.emission - this.capacity
|
|
79
|
+
|
|
80
|
+
if (admit > now)
|
|
81
|
+
return Math.ceil((admit - now) / 1000)
|
|
82
|
+
|
|
83
|
+
if (!this.conditional)
|
|
84
|
+
this.charge(key, entry, tat)
|
|
85
|
+
|
|
86
|
+
return 0
|
|
57
87
|
}
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
|
|
89
|
+
/** Charges what the condition accepts, once the response can be matched against it. */
|
|
90
|
+
public use (input: Context, output: Output): void {
|
|
91
|
+
if (!this.conditional || !this.keys.matches(input, output))
|
|
61
92
|
return
|
|
62
93
|
|
|
63
|
-
// preflight always runs first, and a request it
|
|
94
|
+
// preflight always runs first, and a request it refuses never reaches settle
|
|
64
95
|
const key = this.keyed.get(input) ?? this.keys.get(input)
|
|
96
|
+
const now = Date.now()
|
|
97
|
+
const entry = this.entries.get(key)
|
|
98
|
+
const tat = Math.max(entry?.tat ?? now, now)
|
|
99
|
+
|
|
100
|
+
this.charge(key, entry, tat)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Adds what is worth reporting to the batch, and drops what has gone quiet.
|
|
105
|
+
*
|
|
106
|
+
* The debt is not cleared here but in {@link settled}, so a tick that fails to reach
|
|
107
|
+
* Redis leaves it to be reported by the next one instead of losing it.
|
|
108
|
+
*/
|
|
109
|
+
public flush (now: number, batch: Batch[]): void {
|
|
110
|
+
for (const [key, entry] of this.entries) {
|
|
111
|
+
const delta = Math.round(entry.debt)
|
|
65
112
|
|
|
66
|
-
|
|
113
|
+
if (delta > 0) {
|
|
114
|
+
batch.push({ quotas: this, key, delta })
|
|
67
115
|
|
|
68
|
-
|
|
69
|
-
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
70
118
|
|
|
71
|
-
|
|
72
|
-
|
|
119
|
+
// out of debt and with nothing left to report, a key says no more than an absent one
|
|
120
|
+
if (entry.tat <= now)
|
|
121
|
+
this.entries.delete(key)
|
|
122
|
+
}
|
|
73
123
|
}
|
|
74
124
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
125
|
+
/** Takes the group's debt back, and clears what this process contributed to it. */
|
|
126
|
+
public settled (reported: Batch, debt: number, now: number): void {
|
|
127
|
+
const entry = this.entries.get(reported.key)
|
|
78
128
|
|
|
79
|
-
|
|
80
|
-
|
|
129
|
+
if (entry === undefined) {
|
|
130
|
+
if (debt > 0)
|
|
131
|
+
this.entries.set(reported.key, { tat: now + debt, debt: 0 })
|
|
81
132
|
|
|
82
|
-
private block (key: string): void {
|
|
83
|
-
if (this.blocked[key] === true)
|
|
84
133
|
return
|
|
134
|
+
}
|
|
85
135
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const timer = setTimeout(() => {
|
|
89
|
-
delete this.blocked[key]
|
|
136
|
+
entry.debt -= reported.delta
|
|
90
137
|
|
|
91
|
-
|
|
92
|
-
|
|
138
|
+
// the group has spent at least what this process has, so its debt is the one to keep
|
|
139
|
+
entry.tat = Math.max(entry.tat, now + debt)
|
|
140
|
+
}
|
|
93
141
|
|
|
94
|
-
|
|
95
|
-
this.
|
|
142
|
+
public name (key: string): string {
|
|
143
|
+
return this.prefix + key
|
|
144
|
+
}
|
|
96
145
|
|
|
97
|
-
|
|
146
|
+
private charge (key: string, entry: Entry | undefined, tat: number): void {
|
|
147
|
+
if (entry === undefined)
|
|
148
|
+
this.entries.set(key, { tat: tat + this.emission, debt: this.emission })
|
|
149
|
+
else {
|
|
150
|
+
entry.tat = tat + this.emission
|
|
151
|
+
entry.debt += this.emission
|
|
152
|
+
}
|
|
98
153
|
}
|
|
99
154
|
}
|
|
100
155
|
|
|
156
|
+
/**
|
|
157
|
+
* How often to reconcile, in milliseconds.
|
|
158
|
+
*
|
|
159
|
+
* Reconciling is what bounds the overshoot — between two ticks a gateway is going on
|
|
160
|
+
* what it alone has spent — so it is worth doing an order of magnitude more often than
|
|
161
|
+
* the interval, within reason on either end.
|
|
162
|
+
*/
|
|
163
|
+
function period (interval: number): number {
|
|
164
|
+
return Math.min(Math.max(interval / 10, MIN_PERIOD), MAX_PERIOD)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const MIN_PERIOD = 250
|
|
168
|
+
const MAX_PERIOD = 2000
|
|
169
|
+
|
|
170
|
+
interface Entry {
|
|
171
|
+
/** Theoretical arrival time: when the key is back at zero if nothing more arrives. */
|
|
172
|
+
tat: number
|
|
173
|
+
|
|
174
|
+
/** Milliseconds of debt this process has added and not yet reported. */
|
|
175
|
+
debt: number
|
|
176
|
+
}
|
|
177
|
+
|
|
101
178
|
interface Options {
|
|
102
179
|
keys: Keys
|
|
103
180
|
requests: number
|
|
104
181
|
interval: number
|
|
105
|
-
|
|
106
|
-
counter: Promise<Remote>
|
|
182
|
+
conditional: boolean
|
|
107
183
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Sync } from './Sync'
|
|
2
|
+
import { Quotas } from './Quotas'
|
|
3
|
+
import type { Remote } from '@toa.io/core'
|
|
4
|
+
import type { Configuration } from './Configuration'
|
|
5
|
+
import type { Input as Context } from '../../../../io'
|
|
6
|
+
|
|
7
|
+
let sync: Sync
|
|
8
|
+
let invocations: Input[]
|
|
9
|
+
let context: Context
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.useFakeTimers()
|
|
13
|
+
|
|
14
|
+
invocations = []
|
|
15
|
+
context = createContext()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
sync.dispose()
|
|
20
|
+
|
|
21
|
+
jest.useRealTimers()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should tick as often as the shortest interval asks', async () => {
|
|
25
|
+
sync = new Sync(createStash())
|
|
26
|
+
|
|
27
|
+
const slow = createQuotas({ interval: 60_000 })
|
|
28
|
+
|
|
29
|
+
// on its own a minute reconciles every two seconds, which is the cap
|
|
30
|
+
sync.register(slow)
|
|
31
|
+
slow.check(context, [])
|
|
32
|
+
|
|
33
|
+
await tick()
|
|
34
|
+
|
|
35
|
+
expect(invocations).toHaveLength(0)
|
|
36
|
+
|
|
37
|
+
// a second asks for every 250ms, and the shortest sets the pace for all of them
|
|
38
|
+
sync.register(createQuotas({ interval: 1000 }))
|
|
39
|
+
|
|
40
|
+
await tick()
|
|
41
|
+
|
|
42
|
+
expect(invocations).toHaveLength(1)
|
|
43
|
+
expect(jest.getTimerCount()).toBe(1)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should reconcile every quota in one call', async () => {
|
|
47
|
+
sync = new Sync(createStash())
|
|
48
|
+
|
|
49
|
+
const one = createQuotas()
|
|
50
|
+
const two = createQuotas({ key: [{ method: 'identity' }] })
|
|
51
|
+
|
|
52
|
+
sync.register(one)
|
|
53
|
+
sync.register(two)
|
|
54
|
+
|
|
55
|
+
one.check(context, [])
|
|
56
|
+
two.check(context, [])
|
|
57
|
+
|
|
58
|
+
await tick()
|
|
59
|
+
|
|
60
|
+
expect(invocations).toHaveLength(1)
|
|
61
|
+
expect(invocations[0].keys).toHaveLength(2)
|
|
62
|
+
expect(invocations[0].deltas).toStrictEqual([50, 50])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should not call when there is nothing to report', async () => {
|
|
66
|
+
sync = new Sync(createStash())
|
|
67
|
+
|
|
68
|
+
sync.register(createQuotas())
|
|
69
|
+
|
|
70
|
+
await tick()
|
|
71
|
+
|
|
72
|
+
expect(invocations).toHaveLength(0)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should take the group debt back', async () => {
|
|
76
|
+
sync = new Sync(createStash(() => [1000]))
|
|
77
|
+
|
|
78
|
+
const quotas = createQuotas()
|
|
79
|
+
|
|
80
|
+
sync.register(quotas)
|
|
81
|
+
quotas.check(context, [])
|
|
82
|
+
|
|
83
|
+
await tick()
|
|
84
|
+
|
|
85
|
+
// one request here, but the group is already at capacity
|
|
86
|
+
expect(quotas.check(context, [])).toBeGreaterThan(0)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should clear what it reported', async () => {
|
|
90
|
+
sync = new Sync(createStash())
|
|
91
|
+
|
|
92
|
+
const quotas = createQuotas()
|
|
93
|
+
|
|
94
|
+
sync.register(quotas)
|
|
95
|
+
quotas.check(context, [])
|
|
96
|
+
|
|
97
|
+
await tick()
|
|
98
|
+
await tick()
|
|
99
|
+
|
|
100
|
+
expect(invocations).toHaveLength(1)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should keep what it could not report, and keep serving', async () => {
|
|
104
|
+
sync = new Sync(createStash(() => {
|
|
105
|
+
throw new Error('Redis is unreachable')
|
|
106
|
+
}))
|
|
107
|
+
|
|
108
|
+
const quotas = createQuotas()
|
|
109
|
+
|
|
110
|
+
sync.register(quotas)
|
|
111
|
+
quotas.check(context, [])
|
|
112
|
+
|
|
113
|
+
await tick()
|
|
114
|
+
await tick()
|
|
115
|
+
|
|
116
|
+
expect(invocations).toHaveLength(2)
|
|
117
|
+
expect(invocations[1].deltas).toStrictEqual([50])
|
|
118
|
+
expect(quotas.check(context, [])).toBe(0)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
async function tick (): Promise<void> {
|
|
122
|
+
await jest.advanceTimersByTimeAsync(250)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function createQuotas (properties?: Partial<Configuration>): Quotas {
|
|
126
|
+
const configuration = { key: [{ method: 'path' as const }], requests: 2, interval: 100 }
|
|
127
|
+
|
|
128
|
+
return Quotas.create(Object.assign(configuration, properties))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createContext (properties?: any): Context {
|
|
132
|
+
return Object.assign({ url: new URL('http://localhost/'), identity: { id: 'one' } },
|
|
133
|
+
properties) as unknown as Context
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createStash (reply?: (deltas: number[]) => number[]): Promise<Remote> {
|
|
137
|
+
const remote = {
|
|
138
|
+
invoke: async (endpoint: string, request: { input: Input }): Promise<number[]> => {
|
|
139
|
+
expect(endpoint).toBe('meter')
|
|
140
|
+
|
|
141
|
+
invocations.push(request.input)
|
|
142
|
+
|
|
143
|
+
return reply === undefined ? request.input.deltas : reply(request.input.deltas)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return Promise.resolve(remote as unknown as Remote)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface Input {
|
|
151
|
+
keys: string[]
|
|
152
|
+
deltas: number[]
|
|
153
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { console } from 'openspan'
|
|
2
|
+
import type { Quotas } from './Quotas'
|
|
3
|
+
import type { Remote } from '@toa.io/core'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reconciles every quota of the process with the other gateways, on one tick.
|
|
7
|
+
*
|
|
8
|
+
* Nothing here is on the request path. Each tick takes what the quotas have spent since
|
|
9
|
+
* the last one and sends all of it as a single call, so the cost of staying in step is
|
|
10
|
+
* one round trip a tick — not one per key, and not one per directive. What comes back is
|
|
11
|
+
* the debt the whole group has reached, which the quotas then decide on locally.
|
|
12
|
+
*/
|
|
13
|
+
export class Sync {
|
|
14
|
+
private readonly stash: Promise<Remote>
|
|
15
|
+
private readonly quotas: Quotas[] = []
|
|
16
|
+
private remote: Remote | null = null
|
|
17
|
+
private timer: NodeJS.Timeout | null = null
|
|
18
|
+
private period = Infinity
|
|
19
|
+
|
|
20
|
+
/** Ticks do not overlap: a slow round trip delays reconciling, it does not double it. */
|
|
21
|
+
private reconciling = false
|
|
22
|
+
|
|
23
|
+
public constructor (stash: Promise<Remote>) {
|
|
24
|
+
this.stash = stash
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public register (quotas: Quotas): void {
|
|
28
|
+
this.quotas.push(quotas)
|
|
29
|
+
|
|
30
|
+
// the shortest interval sets the pace, and reconciling the others more often than
|
|
31
|
+
// they asked for only makes them more accurate — an idle quota contributes nothing
|
|
32
|
+
if (quotas.period < this.period) {
|
|
33
|
+
this.period = quotas.period
|
|
34
|
+
|
|
35
|
+
this.start()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public dispose (): void {
|
|
40
|
+
if (this.timer !== null) {
|
|
41
|
+
clearInterval(this.timer)
|
|
42
|
+
|
|
43
|
+
this.timer = null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private start (): void {
|
|
48
|
+
this.dispose()
|
|
49
|
+
|
|
50
|
+
this.timer = setInterval(this.tick, this.period)
|
|
51
|
+
|
|
52
|
+
this.timer.unref()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private readonly tick = (): void => {
|
|
56
|
+
if (this.reconciling)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
const now = Date.now()
|
|
60
|
+
const batch: Batch[] = []
|
|
61
|
+
|
|
62
|
+
for (const quotas of this.quotas)
|
|
63
|
+
quotas.flush(now, batch)
|
|
64
|
+
|
|
65
|
+
if (batch.length === 0)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
this.reconciling = true
|
|
69
|
+
|
|
70
|
+
void this.reconcile(batch).finally(() => {
|
|
71
|
+
this.reconciling = false
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async reconcile (batch: Batch[]): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
this.remote ??= await this.stash
|
|
78
|
+
|
|
79
|
+
const keys = batch.map((entry) => entry.quotas.name(entry.key))
|
|
80
|
+
const deltas = batch.map((entry) => entry.delta)
|
|
81
|
+
|
|
82
|
+
const debts = await this.remote.invoke<number[]>('meter', { input: { keys, deltas } })
|
|
83
|
+
const now = Date.now()
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < batch.length; i++)
|
|
86
|
+
batch[i].quotas.settled(batch[i], debts[i], now)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// what was not reported stays to be reported by the next tick, and until then
|
|
89
|
+
// this gateway keeps throttling on what it alone has seen, and keeps serving
|
|
90
|
+
console.warn('Throttling could not reconcile with the group', error as Error)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface Batch {
|
|
96
|
+
quotas: Quotas
|
|
97
|
+
key: string
|
|
98
|
+
delta: number
|
|
99
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as schemas from './schemas'
|
|
2
2
|
|
|
3
3
|
describe('throttle', () => {
|
|
4
|
-
const rest = { requests: 1, interval: 1
|
|
4
|
+
const rest = { requests: 1, interval: 1 }
|
|
5
5
|
|
|
6
6
|
it('should validate key', () => {
|
|
7
7
|
expect(() => schemas.throttle.validate({ key: 'ip', ...rest })).not.toThrow()
|
|
@@ -22,4 +22,10 @@ describe('throttle', () => {
|
|
|
22
22
|
expect(() => schemas.throttle.validate({ key: 'header', ...rest })).toThrow()
|
|
23
23
|
expect(() => schemas.throttle.validate({ key: { header: 'x-real-ip' }, ...rest })).toThrow()
|
|
24
24
|
})
|
|
25
|
+
|
|
26
|
+
it('should reject a declaration it no longer honours', () => {
|
|
27
|
+
// metering earns the budget back rather than locking a key out, so a `cooldown`
|
|
28
|
+
// left behind is not something to ignore quietly
|
|
29
|
+
expect(() => schemas.throttle.validate({ key: 'ip', cooldown: 1, ...rest })).toThrow()
|
|
30
|
+
})
|
|
25
31
|
})
|
|
@@ -4,6 +4,8 @@ import type { Output } from './io';
|
|
|
4
4
|
import type * as RTD from './RTD';
|
|
5
5
|
export declare class Directives implements RTD.Directives {
|
|
6
6
|
private readonly sets;
|
|
7
|
+
/** the span of a stage depends only on the set, so it is built once per route */
|
|
8
|
+
private readonly spans;
|
|
7
9
|
constructor(sets: RTD.DirectiveSet[]);
|
|
8
10
|
preflight(context: Context, parameters: RTD.Parameter[]): Promise<Output>;
|
|
9
11
|
settle(context: Context, response: OutgoingMessage): Promise<void>;
|
package/transpiled/Directive.js
CHANGED
|
@@ -4,15 +4,22 @@ exports.shortcuts = exports.DirectivesFactory = exports.Directives = void 0;
|
|
|
4
4
|
const openspan_1 = require("openspan");
|
|
5
5
|
class Directives {
|
|
6
6
|
sets;
|
|
7
|
+
/** the span of a stage depends only on the set, so it is built once per route */
|
|
8
|
+
spans;
|
|
7
9
|
constructor(sets) {
|
|
8
10
|
this.sets = sets;
|
|
11
|
+
this.spans = sets.map((set) => ({
|
|
12
|
+
preflight: options(set, 'preflight'),
|
|
13
|
+
settle: options(set, 'settle')
|
|
14
|
+
}));
|
|
9
15
|
}
|
|
10
16
|
async preflight(context, parameters) {
|
|
11
17
|
let output = null;
|
|
12
|
-
for (
|
|
18
|
+
for (let i = 0; i < this.sets.length; i++) {
|
|
19
|
+
const set = this.sets[i];
|
|
13
20
|
if (set.family.preflight === undefined)
|
|
14
21
|
continue;
|
|
15
|
-
const out = await openspan_1.console.span(
|
|
22
|
+
const out = await openspan_1.console.span(this.spans[i].preflight, async () => await set.family.preflight(set.directives, context, parameters));
|
|
16
23
|
if (out === null)
|
|
17
24
|
continue;
|
|
18
25
|
if (output !== null)
|
|
@@ -23,9 +30,11 @@ class Directives {
|
|
|
23
30
|
return output;
|
|
24
31
|
}
|
|
25
32
|
async settle(context, response) {
|
|
26
|
-
for (
|
|
33
|
+
for (let i = 0; i < this.sets.length; i++) {
|
|
34
|
+
const set = this.sets[i];
|
|
27
35
|
if (set.family.settle !== undefined)
|
|
28
|
-
await openspan_1.console.span(
|
|
36
|
+
await openspan_1.console.span(this.spans[i].settle, async () => await set.family.settle(set.directives, context, response));
|
|
37
|
+
}
|
|
29
38
|
}
|
|
30
39
|
dispose() {
|
|
31
40
|
for (const set of this.sets)
|
|
@@ -79,6 +88,9 @@ class DirectivesFactory {
|
|
|
79
88
|
// can key a quota on it, and a request it denies should not reach `io` at all.
|
|
80
89
|
// The rest keep the order they were declared in, the sort being stable.
|
|
81
90
|
sets.sort((a, b) => this.rank(a.family.name) - this.rank(b.family.name));
|
|
91
|
+
// whatever order a family needs among its own directives is fixed here, not per request
|
|
92
|
+
for (const set of sets)
|
|
93
|
+
set.family.arrange?.(set.directives);
|
|
82
94
|
const directives = new Directives(sets);
|
|
83
95
|
this.instances.push(directives);
|
|
84
96
|
return directives;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Directive.js","sourceRoot":"","sources":["../source/Directive.ts"],"names":[],"mappings":";;;AAAA,uCAAoD;AAMpD,MAAa,UAAU;IACJ,IAAI,CAAoB;IAEzC,YAAoB,IAAwB;QAC1C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"Directive.js","sourceRoot":"","sources":["../source/Directive.ts"],"names":[],"mappings":"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"}
|
package/transpiled/Endpoint.js
CHANGED
|
@@ -25,7 +25,6 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.EndpointsFactory = exports.Endpoint = void 0;
|
|
27
27
|
const node_stream_1 = require("node:stream");
|
|
28
|
-
const node_crypto_1 = require("node:crypto");
|
|
29
28
|
const openspan_1 = require("openspan");
|
|
30
29
|
const Mapping_1 = require("./Mapping");
|
|
31
30
|
const http = __importStar(require("./HTTP"));
|
|
@@ -109,13 +108,13 @@ class Endpoint {
|
|
|
109
108
|
}
|
|
110
109
|
if (reply instanceof node_stream_1.Readable)
|
|
111
110
|
return false;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
message.
|
|
111
|
+
/*
|
|
112
|
+
* A reply that carries no version is tagged with a hash of its body. The body is
|
|
113
|
+
* serialized anyway when the response is written, so the tag is computed from what
|
|
114
|
+
* is actually sent rather than from a second serialization of the reply — which
|
|
115
|
+
* makes it specific to the negotiated representation, hence `vary: accept`.
|
|
116
|
+
*/
|
|
117
|
+
message.etag = true;
|
|
119
118
|
return false;
|
|
120
119
|
}
|
|
121
120
|
query(context) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Endpoint.js","sourceRoot":"","sources":["../source/Endpoint.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,6CAAsC;AACtC,
|
|
1
|
+
{"version":3,"file":"Endpoint.js","sourceRoot":"","sources":["../source/Endpoint.ts"],"names":[],"mappings":"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"}
|