| author | |
| committer | |
| log | c4481b9b07d9108d3aa93494a0ec5dd28ed8fd80 |
| tree | 71128b03438be5a14822d029309d1148cb720268 |
| parent | f8ed2a039b7deaa324beda29b44cc74c846311f7 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>2 files changed, 53 insertions(+), 2 deletions(-)
infra/relay/src/environments/ManagedEndpointProvider.test.ts+27-2| ... | @@ -1118,7 +1118,7 @@ describe("ManagedEndpointProvider", () => { | ... | @@ -1118,7 +1118,7 @@ describe("ManagedEndpointProvider", () => { |
| 1118 | }).pipe(Effect.provide(layer)); | 1118 | }).pipe(Effect.provide(layer)); |
| 1119 | }); | 1119 | }); |
| 1120 | 1120 | ||
| 1121 | it.effect("does no Cloudflare work when the registered origin is unchanged", () => { | 1121 | it.effect("only confirms the tunnel exists when the registered origin is unchanged", () => { |
| 1122 | const tunnelCalls: TunnelCall[] = []; | 1122 | const tunnelCalls: TunnelCall[] = []; |
| 1123 | const layer = layerProvider(makePersistentTunnelClient(tunnelCalls)); | 1123 | const layer = layerProvider(makePersistentTunnelClient(tunnelCalls)); |
| 1124 | 1124 | ||
| ... | @@ -1137,7 +1137,31 @@ describe("ManagedEndpointProvider", () => { | ... | @@ -1137,7 +1137,31 @@ describe("ManagedEndpointProvider", () => { |
| 1137 | endpoint: provisioned.endpoint, | 1137 | endpoint: provisioned.endpoint, |
| 1138 | }), | 1138 | }), |
| 1139 | ).toBe("ready"); | 1139 | ).toBe("ready"); |
| 1140 | expect(tunnelCalls).toEqual([]); | 1140 | expect(tunnelCalls).toEqual([{ operation: "get", input: "tunnel-id" }]); |
| 1141 | }).pipe(Effect.provide(layer)); | ||
| 1142 | }); | ||
| 1143 | |||
| 1144 | it.effect("asks for recovery when the recorded tunnel was deleted", () => { | ||
| 1145 | const tunnelCalls: TunnelCall[] = []; | ||
| 1146 | const layer = layerProvider(makePersistentTunnelClient(tunnelCalls)); | ||
| 1147 | |||
| 1148 | return Effect.gen(function* () { | ||
| 1149 | const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider; | ||
| 1150 | const key = { userId: "user_ABC", environmentId: "env_ABC" } as const; | ||
| 1151 | const origin = { localHttpHost: "127.0.0.1", localHttpPort: 3773 } as const; | ||
| 1152 | const provisioned = yield* provider.provision({ ...key, origin }); | ||
| 1153 | // A shutdown release deletes the tunnel but keeps the recorded id; the | ||
| 1154 | // host was killed before it dropped its stored config. | ||
| 1155 | expect(yield* provider.release(key)).toBe(true); | ||
| 1156 | |||
| 1157 | expect( | ||
| 1158 | yield* provider.reconcileOrigin({ | ||
| 1159 | ...key, | ||
| 1160 | tunnelId: provisioned.runtime.tunnelId!, | ||
| 1161 | origin, | ||
| 1162 | endpoint: provisioned.endpoint, | ||
| 1163 | }), | ||
| 1164 | ).toBe("recovery_required"); | ||
| 1141 | }).pipe(Effect.provide(layer)); | 1165 | }).pipe(Effect.provide(layer)); |
| 1142 | }); | 1166 | }); |
| 1143 | 1167 | ||
| ... | @@ -1163,6 +1187,7 @@ describe("ManagedEndpointProvider", () => { | ... | @@ -1163,6 +1187,7 @@ describe("ManagedEndpointProvider", () => { |
| 1163 | }), | 1187 | }), |
| 1164 | ).toBe("ready"); | 1188 | ).toBe("ready"); |
| 1165 | expect(tunnelCalls).toEqual([ | 1189 | expect(tunnelCalls).toEqual([ |
| 1190 | { operation: "get", input: "tunnel-id" }, | ||
| 1166 | { | 1191 | { |
| 1167 | operation: "putConfiguration", | 1192 | operation: "putConfiguration", |
| 1168 | input: { | 1193 | input: { |
infra/relay/src/environments/ManagedEndpointProvider.ts+26| ... | @@ -55,6 +55,7 @@ const ManagedEndpointProvisioningStage = Schema.Literals([ | ... | @@ -55,6 +55,7 @@ const ManagedEndpointProvisioningStage = Schema.Literals([ |
| 55 | "mark-allocation-ready", | 55 | "mark-allocation-ready", |
| 56 | "load-allocation", | 56 | "load-allocation", |
| 57 | "verify-endpoint", | 57 | "verify-endpoint", |
| 58 | "verify-tunnel", | ||
| 58 | "sync-origin", | 59 | "sync-origin", |
| 59 | ]); | 60 | ]); |
| 60 | 61 | ||
| ... | @@ -208,6 +209,7 @@ export interface ManagedEndpointTunnel { | ... | @@ -208,6 +209,7 @@ export interface ManagedEndpointTunnel { |
| 208 | readonly status?: string | null; | 209 | readonly status?: string | null; |
| 209 | readonly createdAt?: string | null; | 210 | readonly createdAt?: string | null; |
| 210 | readonly connsInactiveAt?: string | null; | 211 | readonly connsInactiveAt?: string | null; |
| 212 | readonly deletedAt?: string | null; | ||
| 211 | } | 213 | } |
| 212 | 214 | ||
| 213 | export interface ManagedEndpointTunnelListRequest { | 215 | export interface ManagedEndpointTunnelListRequest { |
| ... | @@ -581,6 +583,30 @@ export const make = Effect.gen(function* () { | ... | @@ -581,6 +583,30 @@ export const make = Effect.gen(function* () { |
| 581 | hostname: allocation.hostname, | 583 | hostname: allocation.hostname, |
| 582 | }); | 584 | }); |
| 583 | } | 585 | } |
| 586 | // A release keeps the recorded tunnel id, so the record alone cannot | ||
| 587 | // tell a live tunnel from one deleted by a shutdown whose host was | ||
| 588 | // killed before it dropped its config. Ask Cloudflare, or the host | ||
| 589 | // starts a connector that can never connect. | ||
| 590 | const recorded = yield* tunnels.get(input.tunnelId).pipe( | ||
| 591 | Effect.asSome, | ||
| 592 | Effect.catchTags({ | ||
| 593 | ManagedEndpointTunnelClientError: (cause) => | ||
| 594 | isManagedEndpointNotFound(cause.cause) | ||
| 595 | ? Effect.succeedNone | ||
| 596 | : Effect.fail( | ||
| 597 | new ManagedEndpointProvisioningFailed({ | ||
| 598 | userId: input.userId, | ||
| 599 | environmentId: input.environmentId, | ||
| 600 | stage: "verify-tunnel", | ||
| 601 | tunnelId: input.tunnelId, | ||
| 602 | cause, | ||
| 603 | }), | ||
| 604 | ), | ||
| 605 | }), | ||
| 606 | ); | ||
| 607 | if (Option.isNone(recorded) || recorded.value.deletedAt) { | ||
| 608 | return "recovery_required"; | ||
| 609 | } | ||
| 584 | if ( | 610 | if ( |
| 585 | allocation.origin?.localHttpHost === input.origin.localHttpHost && | 611 | allocation.origin?.localHttpHost === input.origin.localHttpHost && |
| 586 | allocation.origin.localHttpPort === input.origin.localHttpPort | 612 | allocation.origin.localHttpPort === input.origin.localHttpPort |