Skip to content

Commit 37221bf

Browse files
committed
RPC client: resend to a rediscovered server only on 401/403/404/410; a 504 apply may still be running
1 parent d4560e9 commit 37221bf

3 files changed

Lines changed: 35 additions & 6 deletions

File tree

‎docs/sources.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1281,7 +1281,11 @@ Pending provenance (excluded from the passing-test table): `persists an approved
12811281
| rpc | Authentication rejection rediscovers a replacement in the same call | new (neither copy) | stale status 401 rediscovery succeeds in the same pending call |
12821282
| rpc | Forbidden response rediscovers a replacement in the same call | new (neither copy) | stale status 403 rediscovery succeeds in the same pending call |
12831283
| rpc | Stale route response rediscovers a replacement in the same call | new (neither copy) | stale status 404 rediscovery succeeds in the same pending call |
1284-
| rpc | Unavailable response rediscovers a replacement in the same call | new (neither copy) | stale status 503 rediscovery succeeds in the same pending call |
1284+
| rpc | Gone response rediscovers a replacement in the same call | new (neither copy) | stale status 410 rediscovery succeeds in the same pending call |
1285+
| rpc | Internal error is not resent to another server | new (neither copy) | status 500 is not resent to another server |
1286+
| rpc | Bad gateway is not resent to another server | new (neither copy) | status 502 is not resent to another server |
1287+
| rpc | Unavailable is not resent to another server | new (neither copy) | status 503 is not resent to another server |
1288+
| rpc | An apply past its deadline is not resent to another server | new (neither copy) | status 504 is not resent to another server |
12851289
| rpc | Cached non-strict selection cannot route an exact dead PID to a fallback server | new (neither copy) | cached selections never weaken exactPid refusal for a dead PID |
12861290
| fs | Test cleanup skips completed releases without changing deliberate repeated calls | new (neither copy) | lifetime cleanup skips completed releases but preserves explicit repeated release |
12871291
| fs | Test cleanup joins in-flight releases and releases unreleased resources after failure | new (neither copy) | lifetime cleanup joins in-flight releases and cleans unreleased locks after failure |

‎src/rpc/rpc-client.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,11 @@ async function request<T>(
147147
if (!status) return done(null)
148148
const code = Number(status[1])
149149
if (code < 200 || code >= 300)
150-
return done(
151-
null,
152-
[401, 403, 404, 410, 502, 503, 504].includes(code),
153-
)
150+
// Only statuses meaning "this is not the server you meant" may
151+
// rediscover and resend. A 504 is this server's apply deadline:
152+
// the handler can still be running, so resending it to another
153+
// server could run the command twice. 5xx is never retried.
154+
return done(null, [401, 403, 404, 410].includes(code))
154155
for (const header of headers.slice(1)) {
155156
const colon = header.indexOf(':')
156157
const name = header.slice(0, colon).toLowerCase()

‎test/rpc/client-discovery-cache.test.ts‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ test('connect failure rediscovery succeeds in the same pending call', async () =
130130
})
131131
})
132132

133-
for (const status of [401, 403, 404, 503]) {
133+
for (const status of [401, 403, 404, 410]) {
134134
test(`stale status ${status} rediscovery succeeds in the same pending call`, async () => {
135135
await fixture(async (dir, serve) => {
136136
const replacement = await serve()
@@ -149,6 +149,30 @@ for (const status of [401, 403, 404, 503]) {
149149
})
150150
}
151151

152+
// An apply that hit the server's deadline may still be running there, so the
153+
// client must not resend it to a newer server in the same directory.
154+
for (const status of [500, 502, 503, 504]) {
155+
test(`status ${status} is not resent to another server`, async () => {
156+
await fixture(async (dir, serve) => {
157+
const replacement = await serve()
158+
const busy = await serve(async (socket) => {
159+
await writePortFile(dir, { ...replacement.entry, token: 'new-token' })
160+
socket.end(`HTTP/1.0 ${status} Busy\r\n\r\n{}`)
161+
})
162+
await writePortFile(dir, busy.entry)
163+
const client = createRpcClient(
164+
dir,
165+
process.pid,
166+
undefined,
167+
discoveryCounter(),
168+
)
169+
expect(await client.pending(0)).toEqual([])
170+
expect(busy.calls()).toBe(1)
171+
expect(replacement.calls()).toBe(0)
172+
})
173+
})
174+
}
175+
152176
test('cached selections never weaken exactPid refusal for a dead PID', async () => {
153177
await fixture(async (dir, serve) => {
154178
const live = await serve()

0 commit comments

Comments
 (0)