Skip to content

Commit 2640e6d

Browse files
authored
fix(client): use lightweight connection probe (pingdotgg#4137)
1 parent e34250d commit 2640e6d

8 files changed

Lines changed: 105 additions & 7 deletions

File tree

‎apps/server/src/environment/ServerEnvironment.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
6767

6868
expect(first.environmentId).toBe(second.environmentId);
6969
expect(second.capabilities.repositoryIdentity).toBe(true);
70+
expect(second.capabilities.connectionProbe).toBe(true);
7071
}),
7172
);
7273

‎apps/server/src/environment/ServerEnvironment.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export const make = Effect.gen(function* () {
135135
serverVersion: packageJson.version,
136136
capabilities: {
137137
repositoryIdentity: true,
138+
connectionProbe: true,
138139
},
139140
};
140141

‎apps/server/src/ws.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ const RPC_REQUIRED_SCOPE = new Map<string, AuthEnvironmentScope>([
284284
[ORCHESTRATION_WS_METHODS.subscribeShell, AuthOrchestrationReadScope],
285285
[ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, AuthOrchestrationReadScope],
286286
[ORCHESTRATION_WS_METHODS.subscribeThread, AuthOrchestrationReadScope],
287+
[WS_METHODS.serverProbe, AuthOrchestrationReadScope],
287288
[WS_METHODS.serverGetConfig, AuthOrchestrationReadScope],
288289
[WS_METHODS.serverRefreshProviders, AuthOrchestrationOperateScope],
289290
[WS_METHODS.serverUpdateProvider, AuthOrchestrationOperateScope],
@@ -1238,6 +1239,10 @@ const makeWsRpcLayer = (
12381239
}),
12391240
{ "rpc.aggregate": "orchestration" },
12401241
),
1242+
[WS_METHODS.serverProbe]: (_input) =>
1243+
observeRpcEffect(WS_METHODS.serverProbe, Effect.succeed({}), {
1244+
"rpc.aggregate": "server",
1245+
}),
12411246
[WS_METHODS.serverGetConfig]: (_input) =>
12421247
observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, {
12431248
"rpc.aggregate": "server",

‎packages/client-runtime/src/connection/supervisor.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ describe("EnvironmentSupervisor", () => {
652652
}).pipe(Effect.provide(TestClock.layer())),
653653
);
654654

655-
it.effect("keeps a healthy session when the application becomes active", () =>
655+
it.effect("probes the active session without reconnecting on application activation", () =>
656656
Effect.gen(function* () {
657657
const probeCount = yield* Ref.make(0);
658658
const probeCalled = yield* Deferred.make<void>();

‎packages/client-runtime/src/rpc/session.test.ts‎

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ const SERVER_CONFIG: ServerConfigType = {
109109
serverVersion: "0.0.0-test",
110110
capabilities: {
111111
repositoryIdentity: true,
112+
connectionProbe: true,
112113
},
113114
},
114115
auth: {
@@ -141,6 +142,16 @@ const decodeJson = Schema.decodeUnknownSync(Schema.UnknownFromJsonString);
141142
const decodeRpcRequest = Schema.decodeUnknownSync(RpcRequest);
142143
const encodeJson = Schema.encodeUnknownSync(Schema.UnknownFromJsonString);
143144
const encodeServerConfig = Schema.encodeSync(ServerConfig);
145+
const ENCODED_SERVER_CONFIG = encodeServerConfig(SERVER_CONFIG);
146+
const LEGACY_SERVER_CONFIG = {
147+
...ENCODED_SERVER_CONFIG,
148+
environment: {
149+
...ENCODED_SERVER_CONFIG.environment,
150+
capabilities: {
151+
repositoryIdentity: true,
152+
},
153+
},
154+
};
144155

145156
const makeFactory = Effect.fn("TestRpcSessionFactory.make")(function* () {
146157
const sockets: TestWebSocket[] = [];
@@ -169,9 +180,10 @@ const awaitSocket = Effect.fn("TestRpcSessionFactory.awaitSocket")(function* (
169180

170181
const awaitRequest = Effect.fn("TestRpcSessionFactory.awaitRequest")(function* (
171182
socket: TestWebSocket,
183+
index = 0,
172184
) {
173185
for (let attempt = 0; attempt < 100; attempt += 1) {
174-
const request = socket.sent[0];
186+
const request = socket.sent[index];
175187
if (request) {
176188
return decodeRpcRequest(decodeJson(request));
177189
}
@@ -182,6 +194,7 @@ const awaitRequest = Effect.fn("TestRpcSessionFactory.awaitRequest")(function* (
182194

183195
const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialConfig")(function* (
184196
socket: TestWebSocket,
197+
config: unknown = ENCODED_SERVER_CONFIG,
185198
) {
186199
const request = yield* awaitRequest(socket);
187200
expect(request).toMatchObject({
@@ -195,7 +208,7 @@ const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialCo
195208
requestId: request.id,
196209
exit: {
197210
_tag: "Success",
198-
value: encodeServerConfig(SERVER_CONFIG),
211+
value: config,
199212
},
200213
}),
201214
);
@@ -218,6 +231,30 @@ describe("RpcSessionFactory", () => {
218231
expect(config).toEqual(SERVER_CONFIG);
219232
expect(socket.sent).toHaveLength(1);
220233

234+
const probeFiber = yield* Effect.forkChild(session.probe);
235+
const probeRequest = yield* awaitRequest(socket, 1);
236+
expect(probeRequest).toMatchObject({
237+
_tag: "Request",
238+
tag: WS_METHODS.serverProbe,
239+
payload: {},
240+
});
241+
socket.serverMessage(
242+
encodeJson({
243+
_tag: "Exit",
244+
requestId: probeRequest.id,
245+
exit: {
246+
_tag: "Success",
247+
value: {},
248+
},
249+
}),
250+
);
251+
yield* Fiber.join(probeFiber);
252+
253+
expect(socket.sent.map((request) => decodeRpcRequest(decodeJson(request)).tag)).toEqual([
254+
WS_METHODS.serverGetConfig,
255+
WS_METHODS.serverProbe,
256+
]);
257+
221258
socket.close(1012, "service restart");
222259
const error = yield* Effect.flip(session.closed);
223260

@@ -250,6 +287,45 @@ describe("RpcSessionFactory", () => {
250287
}),
251288
);
252289

290+
it.effect("uses the legacy config RPC for probes when the server lacks the capability", () =>
291+
Effect.scoped(
292+
Effect.gen(function* () {
293+
const { factory, sockets } = yield* makeFactory();
294+
const session = yield* factory.connect(PREPARED);
295+
const readyFiber = yield* Effect.forkChild(session.ready);
296+
const socket = yield* awaitSocket(sockets);
297+
298+
socket.open();
299+
yield* completeInitialConfig(socket, LEGACY_SERVER_CONFIG);
300+
yield* Fiber.join(readyFiber);
301+
302+
const probeFiber = yield* Effect.forkChild(session.probe);
303+
const probeRequest = yield* awaitRequest(socket, 1);
304+
expect(probeRequest).toMatchObject({
305+
_tag: "Request",
306+
tag: WS_METHODS.serverGetConfig,
307+
payload: {},
308+
});
309+
socket.serverMessage(
310+
encodeJson({
311+
_tag: "Exit",
312+
requestId: probeRequest.id,
313+
exit: {
314+
_tag: "Success",
315+
value: LEGACY_SERVER_CONFIG,
316+
},
317+
}),
318+
);
319+
yield* Fiber.join(probeFiber);
320+
321+
expect(socket.sent.map((request) => decodeRpcRequest(decodeJson(request)).tag)).toEqual([
322+
WS_METHODS.serverGetConfig,
323+
WS_METHODS.serverGetConfig,
324+
]);
325+
}),
326+
),
327+
);
328+
253329
it.effect("fails readiness when the websocket never opens", () =>
254330
Effect.gen(function* () {
255331
const { factory, sockets } = yield* makeFactory();

‎packages/client-runtime/src/rpc/session.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ export class RpcSessionFactory extends Context.Service<
4242
type InitialConfigError = Effect.Error<
4343
ReturnType<WsRpcProtocolClient[typeof WS_METHODS.serverGetConfig]>
4444
>;
45+
type ProbeError = Effect.Error<ReturnType<WsRpcProtocolClient[typeof WS_METHODS.serverProbe]>>;
4546

46-
function mapInitialConfigError(error: InitialConfigError): ConnectionAttemptError {
47+
function mapSessionRpcError(error: InitialConfigError | ProbeError): ConnectionAttemptError {
4748
switch (error._tag) {
4849
case "EnvironmentAuthorizationError":
4950
return new ConnectionBlockedError({
@@ -115,12 +116,17 @@ export const make = Effect.gen(function* () {
115116
const client = yield* makeWsRpcProtocolClient.pipe(Effect.provide(protocolContext));
116117
const initialConfig = yield* Effect.cached(
117118
client[WS_METHODS.serverGetConfig]({}).pipe(
118-
Effect.mapError(mapInitialConfigError),
119+
Effect.mapError(mapSessionRpcError),
119120
Effect.withSpan("environment.initialSync"),
120121
),
121122
);
122-
const probe = client[WS_METHODS.serverGetConfig]({}).pipe(
123-
Effect.mapError(mapInitialConfigError),
123+
const probe = initialConfig.pipe(
124+
Effect.flatMap((config) =>
125+
(config.environment.capabilities.connectionProbe === true
126+
? client[WS_METHODS.serverProbe]({})
127+
: client[WS_METHODS.serverGetConfig]({})
128+
).pipe(Effect.mapError(mapSessionRpcError)),
129+
),
124130
Effect.asVoid,
125131
Effect.withSpan("clientRuntime.connection.rpcSession.probe"),
126132
);

‎packages/contracts/src/environment.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type ExecutionEnvironmentPlatform = typeof ExecutionEnvironmentPlatform.T
2222

2323
export const ExecutionEnvironmentCapabilities = Schema.Struct({
2424
repositoryIdentity: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
25+
connectionProbe: Schema.optionalKey(Schema.Boolean),
2526
});
2627
export type ExecutionEnvironmentCapabilities = typeof ExecutionEnvironmentCapabilities.Type;
2728

‎packages/contracts/src/rpc.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ export const WS_METHODS = {
201201
previewAutomationFocusHost: "previewAutomation.focusHost",
202202

203203
// Server meta
204+
serverProbe: "server.probe",
204205
serverGetConfig: "server.getConfig",
205206
serverRefreshProviders: "server.refreshProviders",
206207
serverUpdateProvider: "server.updateProvider",
@@ -246,6 +247,12 @@ export const WsServerRemoveKeybindingRpc = Rpc.make(WS_METHODS.serverRemoveKeybi
246247
error: Schema.Union([KeybindingsConfigError, EnvironmentAuthorizationError]),
247248
});
248249

250+
export const WsServerProbeRpc = Rpc.make(WS_METHODS.serverProbe, {
251+
payload: Schema.Struct({}),
252+
success: Schema.Struct({}),
253+
error: EnvironmentAuthorizationError,
254+
});
255+
249256
export const WsServerGetConfigRpc = Rpc.make(WS_METHODS.serverGetConfig, {
250257
payload: Schema.Struct({}),
251258
success: ServerConfig,
@@ -682,6 +689,7 @@ export const WsSubscribeAuthAccessRpc = Rpc.make(WS_METHODS.subscribeAuthAccess,
682689
});
683690

684691
export const WsRpcGroup = RpcGroup.make(
692+
WsServerProbeRpc,
685693
WsServerGetConfigRpc,
686694
WsServerRefreshProvidersRpc,
687695
WsServerUpdateProviderRpc,

0 commit comments

Comments
 (0)