Skip to content
Open
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
36 changes: 33 additions & 3 deletions apps/desktop/src/backend/DesktopBackendManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
type DesktopBackendBootstrap as DesktopBackendBootstrapValue,
DesktopTelemetryControlMessage,
} from "@t3tools/contracts";
import {
DATABASE_MIGRATIONS_FINISHED_MESSAGE,
DATABASE_MIGRATIONS_STARTED_MESSAGE,
} from "@t3tools/shared/databaseMigrationLog";
import { assert, describe, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Duration from "effect/Duration";
Expand Down Expand Up @@ -688,12 +692,21 @@ describe("DesktopBackendManager", () => {
Effect.scoped(
Effect.gen(function* () {
const reports = yield* Queue.unbounded<DesktopBackendManager.BackendStartupFailure>();
const startInstance = (status: Ref.Ref<number>, onReady?: Effect.Effect<void>) =>
const startInstance = (
status: Ref.Ref<number>,
onReady?: Effect.Effect<void>,
stdout = "",
) =>
makeTestInstance({
spawnerLayer: Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make(() =>
Effect.succeed(makeProcess({ exitCode: Effect.never })),
Effect.succeed(
makeProcess({
exitCode: Effect.never,
stdout: Stream.make(new TextEncoder().encode(stdout)),
}),
),
),
),
httpClientLayer: layerHttpClient((request) =>
Expand All @@ -712,7 +725,24 @@ describe("DesktopBackendManager", () => {
yield* Deferred.await(ready);
assert.isTrue(Option.isNone(yield* Queue.poll(reports)));

yield* startInstance(yield* Ref.make(503));
const migratingStatus = yield* Ref.make(503);
const migrated = yield* Deferred.make<void>();
yield* startInstance(
migratingStatus,
Deferred.succeed(migrated, undefined).pipe(Effect.asVoid),
`INFO ${DATABASE_MIGRATIONS_STARTED_MESSAGE}\n`,
);
yield* TestClock.adjust(Duration.minutes(10));
assert.isTrue(Option.isNone(yield* Queue.poll(reports)));
yield* Ref.set(migratingStatus, 200);
yield* TestClock.adjust(Duration.millis(100));
yield* Deferred.await(migrated);

yield* startInstance(
yield* Ref.make(503),
undefined,
`${DATABASE_MIGRATIONS_STARTED_MESSAGE}\n${DATABASE_MIGRATIONS_FINISHED_MESSAGE}\n`,
);
yield* TestClock.adjust(Duration.seconds(179));
assert.isTrue(Option.isNone(yield* Queue.poll(reports)));
yield* TestClock.adjust(Duration.seconds(1));
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src/backend/DesktopBackendManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
DesktopTelemetryControlMessage,
type DesktopTelemetryControlMessage as DesktopTelemetryControlMessageValue,
} from "@t3tools/contracts";
import { isRunningDatabaseMigrations } from "@t3tools/shared/databaseMigrationLog";
import { waitForHttpReady as waitForHttpReadyShared } from "@t3tools/shared/httpReadiness";

import * as DesktopObservability from "../app/DesktopObservability.ts";
Expand Down Expand Up @@ -971,6 +972,12 @@ export const makeBackendInstance = Effect.fn("makeBackendInstance")(function* (
}),
onReadinessFailure: Effect.fn("desktop.backendInstance.onReadinessFailure")(
function* (error) {
if (isRunningDatabaseMigrations(yield* Ref.get(outputTail))) {
yield* logInstanceWarning("backend is still migrating its database; waiting", {
error: error.message,
});
return;
}
yield* logInstanceWarning("backend readiness check failed during bootstrap", {
error: error.message,
});
Expand Down
8 changes: 7 additions & 1 deletion apps/server/src/persistence/Migrations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as Migrator from "effect/sql/Migrator";
import * as Effect from "effect/Effect";
import * as SqlClient from "effect/sql/SqlClient";
import {
DATABASE_MIGRATIONS_FINISHED_MESSAGE,
DATABASE_MIGRATIONS_STARTED_MESSAGE,
} from "@t3tools/shared/databaseMigrationLog";
import { reconcileV2PreviewMigration } from "./reconcileV2PreviewMigration.ts";

import Migration0001 from "./Migrations/001_OrchestrationEvents.ts";
Expand Down Expand Up @@ -256,6 +260,8 @@ export interface RunMigrationsOptions {
export const runMigrations = Effect.fn("runMigrations")(function* ({
toMigrationInclusive,
}: RunMigrationsOptions = {}) {
// The desktop waits past its readiness budget while this is the server's last output.
if (yield* hasPendingMigrations()) yield* Effect.log(DATABASE_MIGRATIONS_STARTED_MESSAGE);
const includesV2 =
toMigrationInclusive === undefined || toMigrationInclusive >= FIRST_V2_MIGRATION_ID;
const previewMigrations = includesV2 ? yield* reconcileV2PreviewMigration() : [];
Expand All @@ -269,7 +275,7 @@ export const runMigrations = Effect.fn("runMigrations")(function* ({
const migrations = executedMigrations.map(([id, name]) => `${id}_${name}`);
yield* migrations.length === 0
? Effect.logDebug("Database schema is current")
: Effect.log("Migrations ran successfully").pipe(Effect.annotateLogs({ migrations }));
: Effect.log(DATABASE_MIGRATIONS_FINISHED_MESSAGE).pipe(Effect.annotateLogs({ migrations }));

const sql = yield* SqlClient.SqlClient;
const recorded = yield* sql<{
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,10 @@
"types": "./src/testing/symlinks.ts",
"import": "./src/testing/symlinks.ts"
},
"./databaseMigrationLog": {
"types": "./src/databaseMigrationLog.ts",
"import": "./src/databaseMigrationLog.ts"
},
"./httpReadiness": {
"types": "./src/httpReadiness.ts",
"import": "./src/httpReadiness.ts"
Expand Down
11 changes: 11 additions & 0 deletions packages/shared/src/databaseMigrationLog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// The server logs these around pending migrations; the desktop reads them from its output.
// Migrations run synchronously, so a long one blocks the server from writing anything else.
export const DATABASE_MIGRATIONS_STARTED_MESSAGE = "Running database migrations";
export const DATABASE_MIGRATIONS_FINISHED_MESSAGE = "Migrations ran successfully";

export function isRunningDatabaseMigrations(output: string): boolean {
return (
output.lastIndexOf(DATABASE_MIGRATIONS_STARTED_MESSAGE) >
output.lastIndexOf(DATABASE_MIGRATIONS_FINISHED_MESSAGE)
);
}
Loading