mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
fix(demo): save the first baseline after a reopen that seeded too
The demo seed runs in the schema bootstrap, at boot and again after every reopen. Since the baseline save moved out of the seed into DemoResetJob, only the boot read the seed's mark. A restore onto a demo instance with no baseline, whose archive held no admin trips, seeded the example trips during the reopen and then saved nothing, so the hourly reset kept logging "No baseline found"; the mark also stayed set for the rest of the process. Before the move, that path saved the baseline from inside the seed. DatabaseLifecycle now runs listeners after a reopen's schema bootstrap, and DemoResetJob registers one in demo mode that does the same check as at boot: save through the backup port when the seed just ran and no baseline exists. Each check clears the mark. A failed save is logged, never turned into a failed reopen. AdminModule imports DatabaseLifecycleModule from the shared database kernel for it.
This commit is contained in:
@@ -157,13 +157,16 @@ function hasBaseline(): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the boot-time seed put the example trips in during this process.
|
||||
* Whether the demo seed put the example trips in since this mark was last read.
|
||||
*
|
||||
* The first baseline is saved only after a real seed, as it always was: a demo
|
||||
* database that already holds data but has no baseline keeps it that way, and
|
||||
* the hourly reset stays a logged no-op until the admin saves one. The seed
|
||||
* runs before the container exists, so it leaves this mark and DemoResetJob,
|
||||
* which has the backup port, saves the baseline once the app is up.
|
||||
* runs inside the schema bootstrap, at boot before the container exists and
|
||||
* again after every reopen (a restore, the hourly swap), where it has no backup
|
||||
* port either. So it leaves this mark, and DemoResetJob, which has the port,
|
||||
* reads it once the app is up and again after each reopen
|
||||
* (`DatabaseLifecycle.onReopened`), saving the baseline when it is set.
|
||||
*/
|
||||
let seededExampleTrips = false;
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ async function seedDemoData(): Promise<{ adminId: number; demoId: number }> {
|
||||
await seedExampleTrips(demo, adminId, demoId);
|
||||
|
||||
// The first baseline follows this seed and only this seed. DemoResetJob saves
|
||||
// it once the app is up, through the injected backup port this boot-time
|
||||
// seed has no access to.
|
||||
// it once the app is up, or right after the reopen this seed ran in, through
|
||||
// the injected backup port this seed has no access to.
|
||||
markExampleTripsSeeded();
|
||||
return { adminId, demoId };
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ import { AppConfigModule } from '../app-config/app-config.module';
|
||||
// DatabaseBackupModule (nest/backup/) provides once BackupModule is in the
|
||||
// graph. It is not imported here on purpose: see that module for the cycle an
|
||||
// import would close.
|
||||
// DemoResetJob also listens for reopens on DatabaseLifecycle (the shared
|
||||
// database kernel, not a domain), to save the first demo baseline after a
|
||||
// restore's re-bootstrap seeded the example trips.
|
||||
import { DatabaseLifecycleModule } from '../database/database-lifecycle.module';
|
||||
import { Users } from '../../db/entities/Users.entity';
|
||||
import { AuditLog } from '../../db/entities/AuditLog.entity';
|
||||
import { AppSettings } from '../../db/entities/AppSettings.entity';
|
||||
@@ -58,7 +62,7 @@ import { PushSubscriptions } from '../../db/entities/PushSubscriptions.entity';
|
||||
@Module({
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([Users, AuditLog, AppSettings, Addons, PhotoProviders, PhotoProviderFields, DocumentProviders, McpTokens, OauthTokens, Trips, Places, TripFiles, PushSubscriptions]),
|
||||
AppConfigModule, PluginsRuntimeModule, SettingsModule, AuditModule, AddonsModule, AuthModule, NotificationsModule, PackingModule, PermissionsModule, TokensModule, OauthModule, SchedulingModule, KitineraryExtractorModule,
|
||||
AppConfigModule, DatabaseLifecycleModule, PluginsRuntimeModule, SettingsModule, AuditModule, AddonsModule, AuthModule, NotificationsModule, PackingModule, PermissionsModule, TokensModule, OauthModule, SchedulingModule, KitineraryExtractorModule,
|
||||
],
|
||||
controllers: [AdminController],
|
||||
providers: [AdminService, VersionCheckJob, DemoResetJob],
|
||||
|
||||
@@ -4,6 +4,7 @@ import { logInfo, logError } from '../audit/audit-log.logger';
|
||||
import { RuntimeEnvService } from '../app-config/runtime-env.service';
|
||||
import { CronRegistrarService } from '../scheduling/cron-registrar.service';
|
||||
import { DATABASE_BACKUP, type DatabaseBackupStrategy } from '../database/database-backup.interface';
|
||||
import { DatabaseLifecycle } from '../database/database-lifecycle.service';
|
||||
import { withRequestContext } from '../database/request-context';
|
||||
import { hasBaseline, resetDemoUser, saveBaseline, takeExampleTripsSeeded } from '../../demo/demo-reset';
|
||||
|
||||
@@ -15,12 +16,16 @@ import { hasBaseline, resetDemoUser, saveBaseline, takeExampleTripsSeeded } from
|
||||
* side-effect-free, and the close/swap/reopen sequence runs inside
|
||||
* resetDemoUser at tick time, through the injected backup port.
|
||||
*
|
||||
* It also saves the first baseline, on the boot whose seed has just put the
|
||||
* example trips in and only when no baseline exists yet: the same trigger the
|
||||
* save had inside the boot-time demo seed, which runs before the container can
|
||||
* hand the port in. A demo database that already holds data without a baseline
|
||||
* is left alone, so its hourly reset stays a logged no-op as before. Like the
|
||||
* old save, it does not depend on the cron registrar being enabled.
|
||||
* It also saves the first baseline, whenever the demo seed has just put the
|
||||
* example trips in and no baseline exists yet: the same trigger the save had
|
||||
* inside the demo seed, which runs before the container can hand the port in.
|
||||
* That seed runs at boot and again in the schema bootstrap after every reopen
|
||||
* (an admin restore, the hourly swap), so the job checks once the app is up
|
||||
* and again after each reopen, through `DatabaseLifecycle.onReopened`. Each
|
||||
* check clears the seed's mark, so none is left behind. A demo database that
|
||||
* already holds data without a baseline is left alone, so its hourly reset
|
||||
* stays a logged no-op as before. Like the old save, it does not depend on the
|
||||
* cron registrar being enabled.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DemoResetJob implements OnApplicationBootstrap {
|
||||
@@ -29,6 +34,7 @@ export class DemoResetJob implements OnApplicationBootstrap {
|
||||
private readonly registrar: CronRegistrarService,
|
||||
@Inject(DATABASE_BACKUP) private readonly database: DatabaseBackupStrategy,
|
||||
private readonly em: EntityManager,
|
||||
private readonly lifecycle: DatabaseLifecycle,
|
||||
) {}
|
||||
|
||||
async onApplicationBootstrap(): Promise<void> {
|
||||
@@ -39,7 +45,8 @@ export class DemoResetJob implements OnApplicationBootstrap {
|
||||
this.registrar.register('demo-reset', '0 * * * *', () => void this.tick(), { timezone: 'none' });
|
||||
logInfo('Demo hourly reset scheduled');
|
||||
}
|
||||
if (takeExampleTripsSeeded() && !hasBaseline()) await this.saveFirstBaseline();
|
||||
this.lifecycle.onReopened(() => this.saveFirstBaselineIfSeeded());
|
||||
await this.saveFirstBaselineIfSeeded();
|
||||
}
|
||||
|
||||
async tick(): Promise<void> {
|
||||
@@ -50,8 +57,14 @@ export class DemoResetJob implements OnApplicationBootstrap {
|
||||
}
|
||||
}
|
||||
|
||||
/** Boot runs outside any request, so the snapshot gets its own context. */
|
||||
private async saveFirstBaseline(): Promise<void> {
|
||||
/**
|
||||
* Saves the first baseline when the demo seed has just seeded and none exists.
|
||||
* At boot it runs outside any request, so the snapshot gets its own context. Never
|
||||
* throws: at boot a failure must not stop the app, and after a reopen it must
|
||||
* not read as the reopen failing.
|
||||
*/
|
||||
private async saveFirstBaselineIfSeeded(): Promise<void> {
|
||||
if (!takeExampleTripsSeeded() || hasBaseline()) return;
|
||||
try {
|
||||
await withRequestContext({ em: this.em }, () => saveBaseline(this.database));
|
||||
} catch (err: unknown) {
|
||||
|
||||
@@ -22,6 +22,8 @@ import { runSchemaBootstrap } from '../../db/orm';
|
||||
*/
|
||||
@Injectable()
|
||||
export class DatabaseLifecycle {
|
||||
private readonly reopenListeners: Array<() => Promise<void>> = [];
|
||||
|
||||
constructor(private readonly orm: MikroORM) {}
|
||||
|
||||
/**
|
||||
@@ -61,6 +63,20 @@ export class DatabaseLifecycle {
|
||||
await reinitialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `listener` after every reopen, once the reopened file has been through
|
||||
* the schema bootstrap (migrations, seeders, demo seed). For work that has to
|
||||
* follow what that bootstrap did and needs the container, which the bootstrap
|
||||
* itself has no access to: the demo reset job saves the first demo baseline
|
||||
* here when a restore's re-bootstrap seeded the example trips.
|
||||
*
|
||||
* A listener contains its own failures. One that throws turns into the
|
||||
* reopen's error, which a restore reports as "restart required".
|
||||
*/
|
||||
onReopened(listener: () => Promise<void>): void {
|
||||
this.reopenListeners.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kysely caches whatever handle it was given, so without this the ORM would
|
||||
* keep talking to the closed one. Closing and reconnecting sends the driver
|
||||
@@ -75,5 +91,6 @@ export class DatabaseLifecycle {
|
||||
// No pre-migrate snapshot: the file was just unpacked from an archive that
|
||||
// is still there to go back to.
|
||||
await runSchemaBootstrap(this.orm, { snapshot: false });
|
||||
for (const listener of this.reopenListeners) await listener();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user