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
36 changes: 2 additions & 34 deletions .github/workflows/full-regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,11 @@ permissions:
contents: read

jobs:
check-main-update:
name: Check main update
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.decision.outputs.should_run }}
steps:
- uses: actions/checkout@v5
with:
ref: main
fetch-depth: 1

- id: decision
name: Run only when main changed today
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi

main_date="$(TZ=Asia/Shanghai git show -s --format=%cd --date=format:%F HEAD)"
today="$(TZ=Asia/Shanghai date +%F)"
echo "main commit date: ${main_date}"
echo "today: ${today}"
if [ "$main_date" = "$today" ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
else
echo "should_run=false" >> "$GITHUB_OUTPUT"
fi

tests:
name: Full regression tests
needs: check-main-update
if: needs.check-main-update.outputs.should_run == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
ref: main

- uses: pnpm/action-setup@v4

Expand All @@ -65,6 +31,8 @@ jobs:

- name: Run full test suite
run: pnpm test:full-regression
env:
WIKIGRAPH_REGRESSION_TRACE: "1"

- name: Build packages
run: pnpm build
Expand Down
53 changes: 53 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
pull_request:
workflow_dispatch:
inputs:
linuxComparison:
description: Compare Linux workflow phases before and after Archive coordination changes
type: boolean
default: false
nativeDiagnostics:
description: Run native SQLite A/B experiments (known-bad controls intentionally fail)
type: boolean
Expand All @@ -26,8 +30,53 @@ permissions:
contents: read

jobs:
linux-workflows:
name: Linux archive workflow regressions
if: github.event_name != 'workflow_dispatch' || !inputs.linuxComparison
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: node scripts/check-linux-regressions.mjs
- uses: actions/upload-artifact@v4
if: always()
with:
name: linux-workflows
path: test/.reports/linux-workflows.json
if-no-files-found: error

linux-comparison:
name: Linux archive workflow comparison (diagnostic only)
if: github.event_name == 'workflow_dispatch' && inputs.linuxComparison
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: node scripts/diagnose-linux-workflows.mjs
- uses: actions/upload-artifact@v4
if: always()
with:
name: linux-comparison
path: test/.reports/linux-comparison
if-no-files-found: error

packed-sqlite-consumer:
name: Windows packed SDK Node ${{ matrix.node }} and Electron
if: github.event_name != 'workflow_dispatch' || !inputs.linuxComparison
runs-on: windows-latest
timeout-minutes: 25
strategy:
Expand Down Expand Up @@ -232,6 +281,7 @@ jobs:

quality:
name: Code Quality
if: github.event_name != 'workflow_dispatch' || !inputs.linuxComparison
runs-on: ubuntu-latest

steps:
Expand All @@ -252,6 +302,7 @@ jobs:

tests:
name: Tests
if: github.event_name != 'workflow_dispatch' || !inputs.linuxComparison
runs-on: ubuntu-latest

steps:
Expand All @@ -272,6 +323,7 @@ jobs:

windows-archive-coordination:
name: Windows Archive Coordination
if: github.event_name != 'workflow_dispatch' || !inputs.linuxComparison
runs-on: windows-latest

steps:
Expand Down Expand Up @@ -306,6 +358,7 @@ jobs:

package-readiness:
name: End-to-End and Package Readiness
if: github.event_name != 'workflow_dispatch' || !inputs.linuxComparison
runs-on: ubuntu-latest

steps:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 0.9.6

- Release finished database and document async contexts, preventing progressively slower operations on Node 22's async_hooks implementation. Keep the existing 15/20-second regression limits; drain timed-out test bodies before removing their workspace, and require the affected Linux workflows in PR checks. Scheduled full regressions now actually run even when main has no same-day commit.
- Pin the SDK/CLI asynchronous SQLite binding to `npm:@appthreat/sqlite3@7.0.1` (SQLite 3.50.4) to avoid official sqlite3 6.0.1 / SQLite 3.52.0's Windows extended-drive WAL locking regression. Preserve Node >=22.12 and published upstream prebuild installation; no project-owned native distribution or schema migration. Later 7.x versions contain the defect, so the pin is exact; this historical release is not a security-maintenance guarantee.
- Fence Archive reader admission against physical commits, including manifest/schema probes, and distinguish reading, drained settlement, and completed logical operations across processes. Defer ZIP settlement behind active readers without blocking callbacks; the last drained session or recovery reaper persists completed overlays. Keep mutation tokens pinned to their original source reader and discard search caches built over deferred changes.
- Make SDK standalone export drain readers and settle pending writes before copying a coherent snapshot; reject exporting onto the source itself. Directory export uses the existing transaction/recovery lock.
Expand Down
7 changes: 7 additions & 0 deletions docs/en/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ exit codes, and JSON/JSONL rendering belong to `wiki-graph`.

## Concurrent archive access

Short-lived database/document contexts are released when their resources close,
including Node's async-hooks tracking. Custom `HostAsyncContext` providers may
implement the optional synchronous `disable()` hook for equivalent cleanup;
existing providers without the hook remain compatible. The default Node provider
already implements it. Hosts must give each created context independent ownership
and must not invalidate other live contexts when disabling one.

An SDK instance can be called concurrently. Same-archive callbacks may be queued;
cross-process sessions coordinate through the shared Home state. A ZIP reader
retains its mutation-token generation until it closes. If another session finishes
Expand Down
5 changes: 5 additions & 0 deletions docs/zh-CN/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ JSON/JSONL 渲染属于 `wiki-graph`。

## Archive 并发访问

数据库和文档关闭时会释放其短生命周期异步上下文,包括 Node 的
async-hooks 跟踪。自定义 `HostAsyncContext` 可实现可选的同步 `disable()`
钩子以释放同类资源;没有此钩子的现有 Host 仍兼容,默认 Node 实现已提供。
每次创建的上下文应独立拥有资源,释放一个上下文不能影响其他仍在使用的上下文。

同一个 SDK 实例可以并发调用;同一 Archive 的回调可能排队,跨进程会话通过共享
Home 状态协调。ZIP reader 在关闭前保持原 mutation token 对应的数据视图。
若其他会话此时完成逻辑写入,已发布 overlay 会保留在 coordinator 状态中,由最后排空的
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/document/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export class Database {
this.#transactionDepth -= 1;
if (isRootTransaction) {
this.#activeTransactionScope = undefined;
if (this.#closed) this.#operationScope.disable?.();
}
}
});
Expand All @@ -236,6 +237,13 @@ export class Database {

await this.#closeDatabase();
this.#closed = true;
// Node's async_hooks implementation retains each enabled ALS instance
// until disable(). Short-lived coordinator connections otherwise make
// every subsequent Promise progressively more expensive on Node 22.
// A close requested from inside a transaction must keep the reentrant
// scope until its callback exits, so later calls fail as closed instead
// of queuing behind that same callback and deadlocking.
if (this.#transactionDepth === 0) this.#operationScope.disable?.();
});
}

Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/document/directory/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,11 @@ export class DirectoryDocument implements Document {
await this.flush();
await this.#database.close();
} finally {
await this.#fileStore.close();
try {
await this.#fileStore.close();
} finally {
this.#contextScope.disable?.();
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/runtime/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export interface HostAsyncContext<T> {
run<R>(store: T, callback: () => R): R;
enterWith(store: T): void;
getStore(): T | undefined;
/** Release host tracking for a finished context. Existing hosts may omit it. */
disable?(): void;
}

export interface HostAsyncContextProvider {
Expand Down
Loading
Loading