Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 8 additions & 19 deletions apps/server/src/vcs/VcsProjectConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";

Expand All @@ -15,16 +16,9 @@ const ProjectVcsConfig = Schema.Struct({
),
vcsKind: Schema.optional(VcsDriverKind),
});
const isProjectVcsConfig = Schema.is(ProjectVcsConfig);

interface ProjectVcsConfigFile {
readonly vcs?:
| {
readonly kind?: VcsDriverKindType | undefined;
}
| undefined;
readonly vcsKind?: VcsDriverKindType | undefined;
}
const ProjectVcsConfigJson = Schema.fromJsonString(ProjectVcsConfig);
const decodeProjectVcsConfigJson = Schema.decodeUnknownOption(ProjectVcsConfigJson);
type ProjectVcsConfigFile = typeof ProjectVcsConfig.Type;

export interface VcsProjectConfigResolveInput {
readonly cwd: string;
Expand All @@ -45,13 +39,8 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto
return config.vcs?.kind ?? config.vcsKind ?? "auto";
}

function parseConfig(raw: string): ProjectVcsConfigFile | null {
try {
const parsed = JSON.parse(raw) as unknown;
return isProjectVcsConfig(parsed) ? parsed : null;
} catch {
return null;
}
function parseConfig(raw: string): Option.Option<ProjectVcsConfigFile> {
return decodeProjectVcsConfigJson(raw);
}

export const make = Effect.fn("makeVcsProjectConfig")(function* () {
Expand Down Expand Up @@ -90,14 +79,14 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () {
}

const parsed = parseConfig(raw);
if (parsed === null) {
if (Option.isNone(parsed)) {
yield* Effect.logWarning("invalid VCS project config", {
configPath,
});
return "auto" as const;
}

return configuredKind(parsed);
return configuredKind(parsed.value);
});

const resolveKind: VcsProjectConfigShape["resolveKind"] = Effect.fn(
Expand Down
6 changes: 3 additions & 3 deletions packages/ssh/src/tunnel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,9 @@ describe("ssh tunnel scripts", () => {
Effect.result(
waitForHttpReady({
baseUrl: "http://127.0.0.1:41773/",
timeoutMs: 1_000,
intervalMs: 100,
probeTimeoutMs: 250,
timeout: Duration.seconds(1),
interval: Duration.millis(100),
probeTimeout: Duration.millis(250),
}),
),
);
Expand Down
49 changes: 26 additions & 23 deletions packages/ssh/src/tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ import {

export const DEFAULT_REMOTE_PORT = 3773;
const REMOTE_PORT_SCAN_WINDOW = 200;
const SSH_READY_TIMEOUT_MS = 20_000;
const SSH_READY_PROBE_TIMEOUT_MS = 1_000;
const TUNNEL_SHUTDOWN_TIMEOUT_MS = 2_000;
const REMOTE_READY_TIMEOUT_MS = 15_000;
const REMOTE_REUSE_READY_TIMEOUT_MS = 2_000;
const SSH_READY_TIMEOUT = Duration.seconds(20);
const SSH_READY_PROBE_TIMEOUT = Duration.seconds(1);
const TUNNEL_SHUTDOWN_TIMEOUT = Duration.seconds(2);
const REMOTE_READY_TIMEOUT = Duration.seconds(15);
const REMOTE_REUSE_READY_TIMEOUT = Duration.seconds(2);
const DEFAULT_HTTP_READY_TIMEOUT = Duration.seconds(30);
const HTTP_READY_RETRY_INTERVAL = Duration.millis(100);

export interface RemoteT3RunnerOptions {
readonly packageSpec?: string;
Expand Down Expand Up @@ -686,9 +688,9 @@ export function buildRemoteLaunchScript(input?: RemoteT3RunnerOptions): string {
T3_WAIT_READY_SCRIPT: stripTrailingNewlines(REMOTE_WAIT_READY_SCRIPT),
T3_DEFAULT_REMOTE_PORT: String(DEFAULT_REMOTE_PORT),
T3_REMOTE_PORT_SCAN_WINDOW: String(REMOTE_PORT_SCAN_WINDOW),
T3_READY_TIMEOUT_MS: String(REMOTE_READY_TIMEOUT_MS),
T3_REUSE_READY_TIMEOUT_MS: String(REMOTE_REUSE_READY_TIMEOUT_MS),
T3_READY_PROBE_TIMEOUT_MS: String(SSH_READY_PROBE_TIMEOUT_MS),
T3_READY_TIMEOUT_MS: String(Duration.toMillis(REMOTE_READY_TIMEOUT)),
T3_REUSE_READY_TIMEOUT_MS: String(Duration.toMillis(REMOTE_REUSE_READY_TIMEOUT)),
T3_READY_PROBE_TIMEOUT_MS: String(Duration.toMillis(SSH_READY_PROBE_TIMEOUT)),
});
}

Expand Down Expand Up @@ -870,17 +872,18 @@ const readRemoteServerLogTail = Effect.fn("ssh/tunnel.readRemoteServerLogTail")(

export const waitForHttpReady = Effect.fn("ssh/tunnel.waitForHttpReady")(function* (input: {
readonly baseUrl: string;
readonly timeoutMs?: number;
readonly intervalMs?: number;
readonly probeTimeoutMs?: number;
readonly timeout?: Duration.Input;
readonly interval?: Duration.Input;
readonly probeTimeout?: Duration.Input;
readonly path?: string;
}): Effect.fn.Return<void, SshReadinessError, HttpClient.HttpClient> {
const timeoutMs = input.timeoutMs ?? 30_000;
const intervalMs = input.intervalMs ?? 100;
const probeTimeoutMs = input.probeTimeoutMs ?? SSH_READY_PROBE_TIMEOUT_MS;
const retryPolicy = Schedule.spaced(Duration.millis(intervalMs)).pipe(
Schedule.take(Math.max(0, Math.ceil(timeoutMs / intervalMs))),
);
const timeout = Duration.fromInputUnsafe(input.timeout ?? DEFAULT_HTTP_READY_TIMEOUT);
const interval = Duration.fromInputUnsafe(input.interval ?? HTTP_READY_RETRY_INTERVAL);
const probeTimeout = Duration.fromInputUnsafe(input.probeTimeout ?? SSH_READY_PROBE_TIMEOUT);
const timeoutMs = Duration.toMillis(timeout);
const intervalMs = Duration.toMillis(interval);
const probeTimeoutMs = Duration.toMillis(probeTimeout);
const retryPolicy = Schedule.spaced(interval);
const requestUrl = new URL(input.path ?? "/", input.baseUrl).toString();
const client = yield* HttpClient.HttpClient;
const lastProbeFailure = yield* Ref.make<unknown>(null);
Expand All @@ -900,7 +903,7 @@ export const waitForHttpReady = Effect.fn("ssh/tunnel.waitForHttpReady")(functio
Effect.gen(function* () {
attempt += 1;
const responseOption = yield* effect.pipe(
Effect.timeoutOption(Duration.millis(probeTimeoutMs)),
Effect.timeoutOption(probeTimeout),
Effect.mapError(
(cause) =>
new SshReadinessError({
Expand Down Expand Up @@ -953,7 +956,7 @@ export const waitForHttpReady = Effect.fn("ssh/tunnel.waitForHttpReady")(functio
cause,
}),
),
Effect.timeoutOption(Duration.millis(timeoutMs)),
Effect.timeoutOption(timeout),
);

return yield* Option.match(result, {
Expand Down Expand Up @@ -1236,7 +1239,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input:
yield* Effect.raceFirst(
waitForHttpReady({
baseUrl: input.httpBaseUrl,
timeoutMs: SSH_READY_TIMEOUT_MS,
timeout: SSH_READY_TIMEOUT,
}),
exitFailure,
).pipe(
Expand Down Expand Up @@ -1296,7 +1299,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input:
: child
.kill({
killSignal: "SIGTERM",
forceKillAfter: TUNNEL_SHUTDOWN_TIMEOUT_MS,
forceKillAfter: TUNNEL_SHUTDOWN_TIMEOUT,
})
.pipe(Effect.ignore),
),
Expand Down Expand Up @@ -1540,7 +1543,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma
[
tunnelEntry.process.kill({
killSignal: "SIGTERM",
forceKillAfter: TUNNEL_SHUTDOWN_TIMEOUT_MS,
forceKillAfter: TUNNEL_SHUTDOWN_TIMEOUT,
}),
stopRemoteServer(
tunnelEntry.target,
Expand Down Expand Up @@ -1594,7 +1597,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma
remotePort: entry.remotePort,
});
const readinessExit = yield* Effect.exit(
waitForHttpReady({ baseUrl: entry.httpBaseUrl, timeoutMs: 2_000 }),
waitForHttpReady({ baseUrl: entry.httpBaseUrl, timeout: REMOTE_REUSE_READY_TIMEOUT }),
);
if (Exit.isSuccess(readinessExit)) {
yield* Effect.logDebug("ssh.environment.tunnel.reused", {
Expand Down
Loading