Skip to content
Merged
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
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/nichedb",
"version": "0.21.1",
"version": "0.21.2",
"description": "CLI and MCP bridge for NicheDB: browse collections, manage sources and feeds, search items, from any deployment",
"type": "module",
"main": "src/index.js",
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import { createInterface } from 'node:readline';

export const VERSION = '0.21.1';
export const VERSION = '0.21.2';
const DEFAULT_API = process.env.NICHEDB_API ?? 'https://nichedb.dev';
const CONFIG_DIR = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'nichedb');
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/web",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/worker/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/worker",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "niche-db",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"description": "An open platform for large, ever-growing databases of real-time data: sources in, feeds out. Web, API, CLI and MCP on one Postgres.",
Expand Down
2 changes: 1 addition & 1 deletion packages/adapters/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/adapters",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/auth/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/auth",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/config",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/core",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/db",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"exports": {
Expand Down
17 changes: 15 additions & 2 deletions packages/db/src/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,13 +481,26 @@ export async function listRuns(sourceId, { limit = 20 } = {}) {
`;
}

/** Runs that never finished: a container that died mid-fetch. Marked so the UI is honest. */
/**
* Runs that never finished: a container that died mid-fetch. Marked so the
* UI is honest, and the source asked to run again now: startRun had pushed
* its next run a whole cadence out, which for a bulk list of ten thousand
* pages read a month apart meant a redeploy in the wrong minute parked the
* list for a month with nothing to show. A killed run is not a run.
*/
export async function reapStaleRuns({ minutes = 30 } = {}) {
const rows = await sql`
update runs set status = 'error', finished_at = now(), error = 'abandoned (process exited)'
where status = 'running' and started_at < now() - (${`${minutes} minutes`})::interval
returning id
returning id, source_id
`;
const sources = [...new Set(rows.map((r) => r.source_id))];
if (sources.length > 0) {
await sql`
update sources set next_run_at = now(), updated_at = now()
where id = any(${pgArray(sources)}::int[]) and enabled
`;
}
return rows.length;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/enrichers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/enrichers",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/knowledge/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/knowledge",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/notify/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/notify",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/payments/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/payments",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/premium/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/premium",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/queue/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nichedb/queue",
"version": "0.21.1",
"version": "0.21.2",
"private": true,
"type": "module",
"exports": {
Expand Down
65 changes: 65 additions & 0 deletions test/rollout-recovery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,68 @@ describe('the slot a failed lookup costs', () => {
expect(Number(retry.mins)).toBeLessThan(3);
});
});

/**
* The statements `reapStaleRuns` runs, kept identical to the query: a run
* abandoned by a container that died is marked, and its source is asked to
* run again now rather than a whole cadence later.
*/
const REAP = `
update runs set status = 'error', finished_at = now(), error = 'abandoned (process exited)'
where status = 'running' and started_at < now() - ($1)::interval
returning id, source_id`;
const REQUEUE = `
update sources set next_run_at = now(), updated_at = now()
where id = any($1::int[]) and enabled`;

describe('a run killed by a redeploy does not park its source for a cadence', () => {
test('the abandoned run is marked and the source is due again now', async () => {
// A bulk list: read once a month, and its first run began two hours ago.
const id = await source({ adapter: 'opensite', minutesOut: 43_200, cadence: 43_200 });
await one(
`insert into runs (source_id, started_at) values ($1, now() - interval '2 hours') returning id`,
[id],
);
const before = await one(
`select next_run_at > now() + interval '29 days' as parked from sources where id = $1`,
[id],
);
expect(before.parked).toBe(true);
const reaped = await rows(REAP, ['40 minutes']);
expect(reaped.map((r) => r.source_id)).toContain(id);
await rows(REQUEUE, [[...new Set(reaped.map((r) => r.source_id))]]);
const run = await one(`select status, error from runs where source_id = $1`, [id]);
expect(run.status).toBe('error');
expect(run.error).toBe('abandoned (process exited)');
const after = await one(`select next_run_at <= now() as due from sources where id = $1`, [id]);
expect(after.due).toBe(true);
});

test('a run still inside its window, and a paused source, are left alone', async () => {
const fresh = await source({ adapter: 'opensite', minutesOut: 43_200, cadence: 43_200 });
await one(
`insert into runs (source_id, started_at) values ($1, now() - interval '5 minutes') returning id`,
[fresh],
);
const paused = await source({
adapter: 'opensite',
minutesOut: 43_200,
cadence: 43_200,
enabled: false,
});
await one(
`insert into runs (source_id, started_at) values ($1, now() - interval '2 hours') returning id`,
[paused],
);
const reaped = await rows(REAP, ['40 minutes']);
expect(reaped.map((r) => r.source_id)).not.toContain(fresh);
expect(reaped.map((r) => r.source_id)).toContain(paused);
await rows(REQUEUE, [[...new Set(reaped.map((r) => r.source_id))]]);
expect(
(await one(`select next_run_at > now() as later from sources where id = $1`, [fresh])).later,
).toBe(true);
expect(
(await one(`select next_run_at > now() as later from sources where id = $1`, [paused])).later,
).toBe(true);
});
});
Loading