Skip to content

Complete filesystem mount, migration, and recovery - #996

Merged
bootjp merged 33 commits into
mainfrom
design/filesystem-elastickv-foundation
Jul 19, 2026
Merged

Complete filesystem mount, migration, and recovery#996
bootjp merged 33 commits into
mainfrom
design/filesystem-elastickv-foundation

Conversation

@bootjp

@bootjp bootjp commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • add the go-fuse raw filesystem server and opt-in server lifecycle
  • add epoch-fenced, resumable whole-file migration and startup intent recovery
  • add placement metrics, Prometheus alerts, and a Grafana dashboard
  • fix explicit-group reverse key scans and filesystem usage scan ownership edge cases

Behavior and risk

  • file data mutations return a retryable stale-home error while a whole-file move is active
  • create and delete operations now use a durable prepared intent plus an atomic namespace transaction
  • FUSE remains disabled unless --filesystemMount is configured
  • temporary dual placement is accepted only while the durable home state is MIGRATING

Validation

  • go test ./...
  • go test ./adapter ./kv ./internal/fskeys ./internal/filesystem/... ./monitoring -count=1
  • targeted go test -race for filesystem, routing, gRPC scan, and metrics paths
  • golangci-lint --config=.golangci.yaml run --fix
  • Linux amd64 test-binary builds for the server and FUSE adapter
  • Prometheus 3.13.1 promtool check config with 5 filesystem rules
  • Grafana dashboard JSON validation

Design doc

  • docs/design/2026_02_24_partial_filesystem_on_elastickv.md

Author: bootjp

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ファイルシステム用キー、MVCCサービス、FUSEアダプタ、チャンクルーティング、分割境界観測を追加し、DistributionServer、監視レジストリ、関連テスト、設計書を更新した。

Changes

ファイルシステムキーとシャードルーティング

Layer / File(s) Summary
キー形式とチャンク境界の正規化
internal/fskeys/*, kv/shard_key.go
inode、ディレクトリ、チャンク、参照、使用量、意図、移行ジョブのキー生成・復元と、チャンクルート境界への正規化を追加した。
シャードスキャン統合
kv/shard_store.go, kv/*_test.go
ファイルシステムチャンクのルート境界、forward/reverseスキャン、GroupID重複除去、明示グループ逆スキャンを追加した。

MVCCファイルシステムサービス

Layer / File(s) Summary
Service APIとトランザクション操作
internal/filesystem/service.go
ファイル・ディレクトリ作成、属性変更、読み書き、truncate、rename、unlink、readdir、StatFSをMVCCトランザクションとして実装した。
参照、GC、リース管理
internal/filesystem/service.go
open handle、参照フェンス、孤立inode・チャンクのGC、リース更新と期限切れ回収を追加した。
サービス検証
internal/filesystem/service_test.go
疎ファイル、chunk削除、rename制約、GC、リース回収、StatFS、usage更新などを検証した。

FUSEアダプタ境界

Layer / File(s) Summary
FUSE委譲とerrno変換
internal/filesystem/fuseadapter/adapter.go
FUSE操作をCoreへ委譲し、既知エラーをPOSIX errnoへ変換した。読み書き前のlease refresh、keepalive、未対応操作を追加した。
アダプタテスト
internal/filesystem/fuseadapter/adapter_test.go
引数伝播、Lookup順序、client ID、readdir制限、lease失敗時の読み取り停止、errno変換、keepaliveを検証した。

分割境界ルーティングと観測

Layer / File(s) Summary
DistributionServerの境界処理
adapter/distribution_server.go, adapter/distribution_server_test.go
GetRouteSplitRangeでファイルシステムキーを正規化し、ファイル内境界エラー時のみobserverへ通知する処理を追加した。
GRPC逆スキャン経路
adapter/grpc.go, adapter/grpc_test.go, kv/shard_store.go
明示グループ付きのreverse RawScanAtを専用ストア経路へ接続し、未対応時のエラーと返却順を検証した。
ファイルシステムメトリクスの接続
monitoring/*, main.go
split_boundaryなどの理由をPrometheusカウンタへ記録し、RegistryからDistributionServerへobserverを渡す構成を追加した。

補助検証と設計記録

Layer / File(s) Summary
コンパクション完了判定と設計書
adapter/redis_delta_compactor_test.go, docs/design/...
コンパクション待機条件にデルタ削除完了を含め、ファイルシステム設計書の実装済み・未実装項目を更新した。

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FUSEClient
  participant fuseadapterAdapter
  participant filesystemService
  participant Dispatcher
  FUSEClient->>fuseadapterAdapter: Read or Write request
  fuseadapterAdapter->>filesystemService: RefreshOpenHandleLease
  fuseadapterAdapter->>filesystemService: Read or Write
  filesystemService->>Dispatcher: Dispatch transaction
  Dispatcher-->>filesystemService: CoordinateResponse
  filesystemService-->>fuseadapterAdapter: Data or filesystem error
  fuseadapterAdapter-->>FUSEClient: Data and syscall.Errno
Loading
sequenceDiagram
  participant DistributionServer
  participant fskeys
  participant FileSystemMetrics
  DistributionServer->>fskeys: NormalizeSplitBoundary
  fskeys-->>DistributionServer: normalized split key
  DistributionServer->>FileSystemMetrics: ObserveFilePinnedHotspot(split_boundary)
  FileSystemMetrics-->>DistributionServer: increment Prometheus counter
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Filesystem mount, migration, and recovery are all represented in the changes, though the title is broader than the PR’s actual foundation work.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a filesystem layer on top of Elastickv, implementing a core filesystem service, a FUSE session adapter, key formatting utilities, and manual split boundary snapping with associated metrics. The review feedback highlights several critical issues: a bug in extracting gRPC status codes from wrapped errors, atomicity violations in SetAttr, Release, and lease reaping due to split transactions, a performance bottleneck in StatFS caused by scanning the entire keyspace, and an optimization opportunity in the Write path to avoid redundant reads on full chunk overwrites.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread adapter/distribution_server.go
Comment thread internal/filesystem/service.go
Comment thread internal/filesystem/service.go
Comment thread internal/filesystem/service.go
Comment thread internal/filesystem/service.go
Comment thread internal/filesystem/service.go
@bootjp

bootjp commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest-head filesystem review blockers.

Changes:

  • unwrap stacked gRPC errors before checking split-boundary status codes
  • keep SetAttr size truncation and metadata updates in one filesystem transaction
  • keep Release / expired lease reaping ref deletion and orphan GC in one filesystem transaction
  • make Open touch the inode key so new refs conflict with concurrent orphan GC under OCC
  • replace StatFS full keyspace scans with a !fs|usage counter updated atomically with create/delete/write/truncate/GC
  • skip chunk read/readKey for full-chunk writes and add regression coverage

Validation run on this head:

  • go test ./internal/filesystem -count=1 -timeout=120s
  • go test ./adapter -run 'TestDistributionServerSplitRange' -count=1 -timeout=120s\n- go test ./internal/fskeys ./internal/filesystem/... ./adapter ./monitoring -run 'TestExtractRouteKey|TestNormalizeSplitBoundary|TestService|TestDistributionServerSplitRange|TestFileSystemMetrics|TestRegistryReturnsFileSystemObserver' -count=1 -timeout=240s\n- golangci-lint --config=.golangci.yaml run ./internal/fskeys ./internal/filesystem/... ./adapter ./monitoring --timeout=5m\n- go test ./... -run TestNonexistent -count=0\n- git diff --check\n- git verify-commit HEAD\n\nNote: an unfiltered go test ./internal/fskeys ./internal/filesystem/... ./adapter ./monitoring -count=1 -timeout=240s timed out in long-running adapter integration coverage after the targeted adapter/filesystem checks above passed.\n\n@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f140605b5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/filesystem/service.go Outdated
Comment thread internal/filesystem/service.go Outdated
Comment thread internal/filesystem/service.go
Comment thread internal/filesystem/service.go Outdated
Comment thread internal/filesystem/service.go Outdated
Comment thread internal/filesystem/service.go Outdated
Comment thread internal/filesystem/service.go Outdated
Comment thread internal/filesystem/service.go Outdated
Comment thread internal/filesystem/fuseadapter/adapter.go
Comment thread internal/filesystem/fuseadapter/adapter.go
@bootjp

bootjp commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest-head filesystem review findings on a7528bdd5f409c23b159df30a0aedae7acd6ca1a.

Changes:

  • revalidate current open-handle leases before expired-lease reaping deletes refs
  • page lease reaper scans so live refs at the front do not starve later expired refs
  • account full-chunk writes from the stored sparse/trimmed chunk length while keeping blind-write readKeys narrow
  • replace inode-touching open fencing with a ref fence key and retry write conflicts internally
  • propagate ref scan errors and fence observed refs before unlink/orphan GC
  • unlink or orphan overwritten rename targets and preserve no-op rename existence checks
  • preserve sparse tails and scan existing chunk keys for truncate/GC instead of logical sparse ranges
  • update mtime for size-only SetAttr
  • map filesystem OCC conflicts to EAGAIN and refresh FUSE handle leases before handle I/O

Validation:

  • go test ./internal/fskeys ./internal/filesystem/... -count=1 -timeout=180s
  • go test ./adapter -run 'TestDistributionServerSplitRange' -count=1 -timeout=120s
  • go test ./internal/fskeys ./internal/filesystem/... ./adapter ./monitoring -run 'TestRefFence|TestService|TestErrno|TestAdapter|TestDistributionServerSplitRange|TestFileSystemMetrics|TestRegistryReturnsFileSystemObserver' -count=1 -timeout=240s
  • golangci-lint --config=.golangci.yaml run ./internal/fskeys ./internal/filesystem/... ./adapter ./monitoring --timeout=5m
  • go test ./... -run TestNonexistent -count=0
  • git diff --check
  • git verify-commit HEAD

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a7528bdd5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/filesystem/service.go Outdated
Comment on lines +797 to +799
if replacedEntry.Type == TypeDirectory {
return nil, ErrIsDir
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject directory renames over files

When the source entry is a directory and newName already names a regular file, this helper treats the target as replaceable because it only rejects directory targets. The caller then overwrites the file's dirent with the directory and unlinks/GCs the file, so rename(dir, file) succeeds instead of returning a type error; please compare the source and target types before allowing replacement.

Useful? React with 👍 / 👎.

Comment thread internal/filesystem/service.go Outdated
Comment on lines +1517 to +1520
err := s.scanExistingChunks(ctx, meta, ts, start, func(pair *store.KVPair) error {
delta = delta.merge(usageDelta{bytesSub: uint64(len(pair.Value))})
elems = append(elems, &kv.Elem[kv.OP]{Op: kv.Del, Key: pair.Key})
readKeys = append(readKeys, pair.Key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Page chunk deletes before building the transaction

For truncating a file with more than 10,000 stored chunks (about 40 GiB at the default 4 MiB chunk size), this loop appends every chunk key to ReadKeys; the coordinator rejects transactions once len(ReadKeys) > 10_000, so large-file truncation can fail instead of deleting the tail. The same unbounded read set is used by file GC below, so unlink/release/rename-over-target can hit the same limit; please page these deletes/usage updates or avoid a single read set that grows with the whole file.

Useful? React with 👍 / 👎.

Comment thread kv/shard_key.go
Comment thread internal/filesystem/service.go Outdated
}
end := prefixEnd(prefix)
for {
page, err := s.store.ScanAt(ctx, start, end, statFSScanPageSize, ts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route filesystem chunk scans through the chunk route domain

When this service is backed by kv.ShardStore, chunk writes are stored on the group selected by routeKey(fskeys.ChunkKey(...)) (the virtual !fs|route|chk|... key), but this raw !fs|chk|... range scan is routed by ShardStore.ScanAt using the raw scan bounds. In a multi-range deployment where the raw chunk prefix and the virtual chunk-route prefix resolve to different groups, truncate/GC scans return no chunks, leaving deleted file data behind and usage deltas wrong; route these chunk prefix scans through the same filesystem route domain or scan the owning group directly.

Useful? React with 👍 / 👎.

@bootjp
bootjp marked this pull request as ready for review July 10, 2026 16:03
@bootjp

bootjp commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest filesystem foundation blockers and merged current main.

Changes:

  • route filesystem chunk scans through the virtual chunk route domain
  • normalize filesystem chunk keys in Distribution.GetRoute via kv.RouteKey
  • reject directory rename over file targets
  • page chunk deletes before follow-up transactions so truncate/GC does not build unbounded read sets
  • keep lint-only helpers small enough for the current lint config

Validation:

  • go test ./internal/filesystem ./internal/fskeys ./kv ./adapter -run 'TestServiceRenameDirectoryOverFileIsRejected|TestServiceTruncatePagesLargeChunkDeletes|TestRouteKey|TestShardStoreScanAt_RoutesFilesystemChunkScansByChunkRouteKey|TestDistributionServerGetRoute_NormalizesFilesystemChunkKeys' -count=1 -timeout=180s\n- go test ./internal/filesystem ./internal/fskeys ./kv -count=1 -timeout=240s\n- go test ./adapter -run 'TestDistributionServerGetRoute|TestDistributionServerManualSplitSnapsFilesystemChunkBoundary|TestDistributionServerListRoutes' -count=1 -timeout=180s\n- golangci-lint run ./internal/filesystem ./internal/fskeys ./kv ./adapter --timeout=5m\n- git diff --check\n\n@codex review\n@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

@bootjp Thanks for the update — I see the latest changes addressing route-key normalization for filesystem chunk scans, the directory-over-file rename rejection, and paginated chunk deletes to bound transaction read sets. I'll re-review the current head now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6915d4680a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/filesystem/service.go Outdated
var delta usageDelta
var chunkDeletes *chunkDeletePlan
if replacedMeta != nil {
replacedElems, replacedReads, replacedDelta, plan, err := s.unlinkFileTxnParts(ctx, ts, replacedMeta, now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use directory semantics for rename targets

When renaming a directory over an existing directory, replacedMeta is a directory but this path always invokes unlinkFileTxnParts. That helper never checks that the target directory is empty and only decrements Nlink, so rename(dirA, dirB) can overwrite a non-empty target dentry while leaving dirB's inode/children orphaned; even empty directory targets remain counted instead of having their inode/home/dir-version rows removed. Directory replacements need the rmdir semantics here.

Useful? React with 👍 / 👎.

Comment thread internal/filesystem/service.go Outdated
Comment thread internal/filesystem/service.go Outdated
Comment on lines +1933 to +1934
ts, err := reader.LinearizableRead(ctx)
return ts, errors.Wrap(err, "filesystem linearizable read")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Read filesystem snapshots at MVCC timestamps

When the service is wired to the production kv.ShardedCoordinator, this type assertion calls LinearizableRead and uses the returned Raft applied index as the MVCC read timestamp for GetAt/ScanAt. Filesystem writes commit at HLC/MVCC timestamps, not Raft indexes, so after InitializeRoot or any write with an HLC commit timestamp greater than the applied index, the next filesystem read can miss the committed version and report ErrNotFound or stale metadata. Use the store's MVCC watermark/snapshot timestamp after fencing instead of the linearizable-read index.

Useful? React with 👍 / 👎.

Comment thread kv/shard_store.go Outdated
Comment on lines +240 to +241
if routeStart, routeEnd, ok := fskeys.ChunkScanRouteBounds(start, end); ok {
return s.engine.GetIntersectingRoutes(routeStart, routeEnd), false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge De-duplicate same-group chunk scan routes

This filesystem chunk scan path returns all intersecting virtual routes with clampToRoutes=false, but the forward scan loop scans each returned route's GroupID without de-duplicating. After SplitRange, child routes normally keep the same backing group, so a broad chunk scan such as the ChunkAllPrefix scan used by the StatFS fallback will scan the same store once per child route and return duplicate chunks, double-counting bytes and potentially duplicating API results.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest filesystem foundation review findings on 8051cfcf9ea087948d07208e6a9080ba2d074a6b.

Changes:

  • use rmdir semantics when renaming a directory over an existing directory, including non-empty target rejection and target inode/home/dir-version removal
  • re-derive EOF chunk cleanup for non-shrinking Truncate / SetAttr(Size) so retrying the same size can reclaim leftover chunk pages and usage accounting
  • fence filesystem reads with LinearizableRead but use the store MVCC watermark for GetAt / ScanAt timestamps
  • de-duplicate same-group filesystem chunk scan routes in ShardStore
  • harden the urgent delta compactor pagination regression to wait for both base meta folding and delta-key cleanup

Validation:

  • go test ./internal/filesystem ./kv ./adapter -run 'TestService(ReadTSUsesMVCCTimestampAfterLinearizableFence|RenameDirectoryOverEmptyDirectoryRemovesTarget|RenameDirectoryOverNonEmptyDirectoryIsRejected|RenameDirectoryOverFileIsRejected|TruncateReclaimsStaleChunksAtCurrentEOF|TruncateGrowReclaimsStaleChunksBeforeExpose|TruncatePagesLargeChunkDeletes)|TestShardStoreScanAt_(RoutesFilesystemChunkScansByChunkRouteKey|DeduplicatesFilesystemChunkRoutesByGroup)|TestDeltaCompactor_UrgentCompactionPagination' -count=1 -timeout=240s
  • go test ./internal/fskeys ./internal/filesystem/... ./kv -count=1 -timeout=240s
  • go test ./adapter -run 'TestDeltaCompactor' -count=1 -timeout=120s
  • go test -race ./adapter -run TestDeltaCompactor_UrgentCompactionPagination -count=1 -timeout=120s
  • golangci-lint run ./internal/filesystem ./internal/fskeys ./kv ./adapter --timeout=5m
  • go test ./... -run TestNonexistent -count=0 -timeout=300s
  • git diff --check
  • git verify-commit HEAD

Note: a full go test ./internal/fskeys ./internal/filesystem/... ./kv ./adapter -count=1 -timeout=300s timed out in long-running adapter raft/gRPC integration coverage; the delta compactor failure path from CI and the touched filesystem/kv packages passed in the targeted runs above.

@codex review

@bootjp

bootjp commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Current HEAD 8051cfcf9ea087948d07208e6a9080ba2d074a6b has no remaining unresolved non-outdated review threads. The stale anchors on older commits are skipped under the latest-head rule.

The remaining merge blocker is GitHub Actions status: several jobs are still reported in progress even though the Jepsen log reached successful cleanup. I am continuing with the CI gate separately.

@codex review
@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

@bootjp Understood — thanks for confirming there are no remaining unresolved non-outdated threads on this head, and for noting the CI/Actions status is being tracked separately. I'll kick off a review of the current head now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@bootjp

bootjp commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Pushed signed CI refresh commit 6cdc4470f5fe250fb7b3da41eaf0f898e7f3eb58 because the prior CodeQL dynamic run was stuck/cancelled and GitHub would not allow rerun.

No file content changed; latest-head unresolved non-outdated review threads remain at 0. New CI is running on this head.

@codex review
@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

@bootjp Understood — thanks for the update. Since no file content changed with this CI refresh commit and the unresolved non-outdated thread count remains at 0, I'll kick off a review of the current head to confirm.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
monitoring/filesystem.go (1)

1-46: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

fsPinnedHotspotReasonSplitBoundaryadapter.DistributionFilePinnedHotspotSplitBoundary が同じ値 "split_boundary" を重複定義している。

一方が変更された場合、メトリクスは normalizeFilePinnedHotspotReason によって "unknown" にフォールバックされるため実害はないが、意図しないデグレードの原因となる。定数の同期を検出するテストを追加するか、共通の定数を参照することを推奨する。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@monitoring/filesystem.go` around lines 1 - 46, Duplicate the "split_boundary"
reason definition shared by fsPinnedHotspotReasonSplitBoundary and
adapter.DistributionFilePinnedHotspotSplitBoundary. Prefer referencing a single
exported shared constant from normalizeFilePinnedHotspotReason and its callers;
alternatively add a test asserting both constants remain synchronized.
adapter/distribution_server.go (1)

211-222: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

isSplitBoundaryError のエラー判定が文字列マッチに依存している。

strings.Contains(err.Error(), errDistributionSplitKeyAtBoundary.Error()) は、grpcStatusError がセンチネルエラーのメッセージ文字列のみを保持する設計上、現状では妥当な選択だが、エラーメッセージの変更に対して脆弱である。将来的に grpcStatusError が元のエラーを wrap するように改修された場合、errors.Is への移行を推奨する。

また status.Code(errors.Cause(err))err.Error() で unwrap の有無が不整合だが、現状では validateSplitKey がエラーを wrap せず返すため問題ない。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adapter/distribution_server.go` around lines 211 - 222, isSplitBoundaryError
の判定がエラーメッセージ文字列に依存している。grpcStatusError
が元エラーを保持・wrapする実装へ移行し、errDistributionSplitKeyAtBoundary を errors.Is
で判定できるようにする。あわせて status.Code とセンチネル判定で同じ unwrap 済みエラーを利用し、エラーの扱いを一貫させる。
internal/filesystem/service.go (2)

375-375: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

コーディングガイドライン: //nolint ディレクティブの使用について。

SetAttrReadWriteRenameReaddircreateNodeunlink//nolint:cyclop が設定されています。各関数のコメントで正当な理由が説明されていますが、コーディングガイドラインでは「//nolint の使用を避け、リファクタリングを推奨する」と明記されています。各関数の分岐をヘルパー関数へ抽出することで cyclop 複雑度を下げることを検討してください。ただし、現状のコメントによる正当化も妥当と見なせるため、必須ではありません。

As per coding guidelines: **/*.go: Avoid using //nolint directives; refactor instead in Go code

Also applies to: 523-523, 589-589, 759-759, 923-923, 1361-1361, 1451-1451

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/filesystem/service.go` at line 375, Refactor SetAttr, Read, Write,
Rename, Readdir, createNode, and unlink to reduce cyclop complexity by
extracting their conditional branches into focused helper functions, then remove
the corresponding //nolint:cyclop directives.

Source: Coding guidelines


613-613: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

directoryInitialNlink を容量計算の定数として再利用している点が分かりにくい。

last - first + uint64(directoryInitialNlink)last - first + 2 と等価で、チャンク要素 + inode メタデータ + usage 更新の容量ヒントを意図していますが、ディレクトリのリンク数定数を流用する意味的接続が不明確です。専用の定数を定義するか、インラインで + 2 とコメントを添えることを推奨します。

♻️ 提案する修正
 const (
 	// ...
 	directoryInitialNlink     uint32 = 2
+	writeTxnMetaElemCount      uint64 = 2 // inode metadata + usage update
 )
-	elemCap, err := checkedInt(last - first + uint64(directoryInitialNlink))
+	elemCap, err := checkedInt(last - first + writeTxnMetaElemCount)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/filesystem/service.go` at line 613, Replace the reuse of
directoryInitialNlink in the capacity calculation near checkedInt with a
semantically named dedicated constant representing the two additional capacity
entries, or use an explicit +2 with a concise comment documenting the chunk,
inode metadata, and usage update capacity hint; preserve the existing checkedInt
behavior.
internal/filesystem/fuseadapter/adapter.go (1)

208-222: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

未対応操作の errno が一部一貫していない点を確認してください。

SymlinkEOPNOTSUPPReadlinkENOSYS を返しています。両者とも「未実装」ですが、errno が異なります。FUSE クライアントの実装によっては挙動が変わる可能性があります。意図的な選択であれば問題ありませんが、ReadlinkEOPNOTSUPP に統一することを検討してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/filesystem/fuseadapter/adapter.go` around lines 208 - 222, 未実装操作の
errno が不統一です。Adapter の Symlink と同様に Readlink も syscall.EOPNOTSUPP
を返すよう変更し、未対応操作のエラーコードを統一してください。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/filesystem/service.go`:
- Around line 1733-1748: deleteChunkPages currently retries indefinitely when
deleteChunkPage reports a write conflict. Add a bounded retry counter within
deleteChunkPages, define an appropriate maximum, and return an error once the
limit is exceeded while preserving existing handling for successful completion
and other errors.

---

Nitpick comments:
In `@adapter/distribution_server.go`:
- Around line 211-222: isSplitBoundaryError
の判定がエラーメッセージ文字列に依存している。grpcStatusError
が元エラーを保持・wrapする実装へ移行し、errDistributionSplitKeyAtBoundary を errors.Is
で判定できるようにする。あわせて status.Code とセンチネル判定で同じ unwrap 済みエラーを利用し、エラーの扱いを一貫させる。

In `@internal/filesystem/fuseadapter/adapter.go`:
- Around line 208-222: 未実装操作の errno が不統一です。Adapter の Symlink と同様に Readlink も
syscall.EOPNOTSUPP を返すよう変更し、未対応操作のエラーコードを統一してください。

In `@internal/filesystem/service.go`:
- Line 375: Refactor SetAttr, Read, Write, Rename, Readdir, createNode, and
unlink to reduce cyclop complexity by extracting their conditional branches into
focused helper functions, then remove the corresponding //nolint:cyclop
directives.
- Line 613: Replace the reuse of directoryInitialNlink in the capacity
calculation near checkedInt with a semantically named dedicated constant
representing the two additional capacity entries, or use an explicit +2 with a
concise comment documenting the chunk, inode metadata, and usage update capacity
hint; preserve the existing checkedInt behavior.

In `@monitoring/filesystem.go`:
- Around line 1-46: Duplicate the "split_boundary" reason definition shared by
fsPinnedHotspotReasonSplitBoundary and
adapter.DistributionFilePinnedHotspotSplitBoundary. Prefer referencing a single
exported shared constant from normalizeFilePinnedHotspotReason and its callers;
alternatively add a test asserting both constants remain synchronized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 56561c29-c6e8-49ee-b0f9-89f2d556deef

📥 Commits

Reviewing files that changed from the base of the PR and between cd58354 and 8051cfc.

📒 Files selected for processing (18)
  • adapter/distribution_server.go
  • adapter/distribution_server_test.go
  • adapter/redis_delta_compactor_test.go
  • docs/design/2026_02_24_partial_filesystem_on_elastickv.md
  • internal/filesystem/fuseadapter/adapter.go
  • internal/filesystem/fuseadapter/adapter_test.go
  • internal/filesystem/service.go
  • internal/filesystem/service_test.go
  • internal/fskeys/keys.go
  • internal/fskeys/keys_test.go
  • kv/shard_key.go
  • kv/shard_key_test.go
  • kv/shard_store.go
  • kv/shard_store_test.go
  • main.go
  • monitoring/filesystem.go
  • monitoring/filesystem_test.go
  • monitoring/registry.go

Comment thread internal/filesystem/service.go
@bootjp

bootjp commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest non-outdated filesystem review thread on f6f76f6ddb743ab9444759ebc2035d811dac36fc.

Changes:

  • bounded consecutive chunk-delete write-conflict retries so cleanup cannot loop forever under persistent OCC conflicts
  • preserved the propagated store.ErrWriteConflict classification so existing callers, including FUSE errno conversion, keep treating the bounded-failure path as retryable
  • added regression coverage with a dispatcher that returns write conflicts until the retry limit is reached

Caller audit:

  • deleteChunkPages callers all propagate the error through the existing filesystem operation paths
  • store.ErrWriteConflict remains visible through wrapping for the existing EAGAIN mapping

Validation:

  • go test ./internal/filesystem -run 'TestServiceDeleteChunkPagesStopsAfterWriteConflictRetryLimit|TestServiceTruncatePagesLargeChunkDeletes|TestServiceTruncateReclaimsStaleChunksAtCurrentEOF' -count=1 -timeout=180s\n- go test ./internal/filesystem ./internal/fskeys -count=1 -timeout=240s\n- golangci-lint run ./internal/filesystem ./internal/fskeys --timeout=5m\n- go test ./... -run TestNonexistent -count=0 -timeout=300s\n- git diff --check\n- git verify-commit HEAD\n\n@codex review\n@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

@bootjp Thanks for the update — I see the bounded retry limit added to deleteChunkPages for consecutive write-conflict retries, the preserved store.ErrWriteConflict classification so the FUSE EAGAIN mapping still treats bounded failures as retryable, and the new regression test with a dispatcher that returns write conflicts until the retry limit is hit. I'll review the current head now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f6f76f6ddb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/filesystem/service.go Outdated
Comment thread internal/fskeys/keys.go Outdated
Comment thread kv/shard_store.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a08f9069be

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread kv/shard_store.go Outdated

func (s *ShardStore) routesForFilesystemChunkScan(start []byte, end []byte) ([]distribution.Route, bool) {
if routeStart, routeEnd, ok := fskeys.ChunkScanRouteBounds(start, end); ok {
return dedupeRoutesByGroup(s.engine.GetIntersectingRoutes(routeStart, routeEnd)), true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep captured chunk routes for backup materialization

This helper feeds NewBackupScanner, so deduplicating the chunk route list here drops catalog ranges before the scanner captures its snapshot. When a chunk-prefix backup spans multiple virtual file routes that share a group, the group scan can still return chunks from the later routes, but after a catalog change materializeRouteForKey no longer has the captured route containing kv.RouteKey(item.key) and can skip those chunks instead of reading them from the old group. Keep the full route list for the scanner and dedupe only in the per-group scan dispatch path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bce58eb. routesForFilesystemChunkScan now preserves every captured route range for backup materialization. Normal scan dispatch still deduplicates by group through seenGroups, so this changes snapshot metadata retention without issuing duplicate group scans. Added TestBackupScannerRetainsAllCapturedChunkRanges.

Comment thread internal/filesystem/service.go Outdated
chunkKey := fskeys.ChunkKey(meta.HomeSlot, inode, idx)
chunkStart := idx * chunkSize
writeStart := max(offset, chunkStart)
writeEnd := min(end, chunkStart+chunkSize)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Saturate chunk end before taking min

When offset lands in the last address-space chunk (for example after SetAttr grows a sparse file near math.MaxUint64), chunkStart+chunkSize wraps before min is applied. That makes writeEnd less than writeStart, so the subsequent offset arithmetic returns ErrInvalid even though offset+len(data) was valid; use checked or saturating addition for the chunk boundary.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bce58eb. Both Read and Write now use checked saturating chunk-end arithmetic, including the full-chunk predicate. The caller/path audit found both arithmetic sites and no remaining chunkStart + chunkSize expression. Added a MaxUint64 final-chunk read/write regression test.

Comment thread kv/backup_scan.go Outdated
Comment on lines +149 to +151
route, ok := s.store.engine.GetRoute(key)
if ok && route.GroupID == item.route.GroupID {
return route, true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid live catalog when materializing backups

BackupScanner is supposed to materialize from the route snapshot captured at creation time, but this fallback consults the live engine after those routes fail to contain the key. If a backup starts while x belongs to group 2, then the catalog later moves x to group 1, a stale/off-route version of x found on group 1 can now pass this same-group check and be included even though the captured route says it should be read from group 2 or skipped. Keep materialization decisions limited to item.route/s.routes so catalog changes after scanner creation cannot change the backup contents.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bce58eb. materializeRouteForKey no longer consults the live routing engine and resolves keys only against the scanner-captured route snapshot. Added TestBackupScannerDoesNotUseLiveCatalogForMaterialization to cover a catalog change after scanner creation.

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Updated PR #996 at bce58eb950833c709aa45360188c7b1a6f2b55d0.

Resolved the latest-head findings:

  • preserve every captured filesystem chunk route for backup materialization while deduplicating only scan dispatch by group
  • remove the live routing catalog fallback from backup key materialization
  • use saturating chunk-end arithmetic in both filesystem Read and Write, including the full-chunk predicate

Audit:

  • all filesystem chunk-end arithmetic call sites were checked; no chunkStart + chunkSize remains
  • backup scanner route capture and materialization paths were checked; catalog lookup is no longer used after scanner creation
  • normal scan dispatch still deduplicates repeated group routes through seenGroups

Verification:

  • go test ./internal/filesystem/... -count=1 -timeout=300s
  • go test ./kv -count=1 -timeout=600s
  • targeted filesystem and backup scanner race tests
  • golangci-lint --config=.golangci.yaml run ./internal/filesystem/... ./kv --timeout=5m
  • go test ./... -run '^$' -count=1 -timeout=600s
  • signed commit verified for bootjp <contact@bootjp.me>

@codex review

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

elastickv/kv/backup_scan.go

Lines 211 to 213 in bce58eb

if write > 0 && bytes.Equal(out[write-1].key, item.key) {
out[write-1] = item
continue

P1 Badge Keep the live route when coalescing backup keys

When a stale old-owner copy of the same key is still present on a route that sorts after the current owner (for example after a range move from the later group back to an earlier one), this duplicate-key branch overwrites the earlier/live routedScanKey with the later stale route. materializeRouteForKey then rejects that stale route because the key no longer belongs to that group, so the backup page drops the live key entirely. Prefer the item whose captured route contains routeKey(item.key) instead of blindly replacing duplicates.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/filesystem/service.go Outdated
Comment on lines +2552 to +2554
end, overflow := addUint64(offset, size)
if overflow {
return 0, ErrInvalid

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp overflowing reads at EOF

When a file has been sparsely extended near math.MaxUint64 (for example via Truncate) and a caller reads from a valid offset close to EOF with a normal request size larger than the remaining bytes, offset + size can overflow here before the code gets a chance to clamp to fileSize. That makes a valid tail read return ErrInvalid/EINVAL instead of the remaining bytes; use saturating end arithmetic for reads before applying the EOF clamp.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4685081. Read end calculation now uses saturating uint64 addition before clamping to EOF, so a valid tail read near MaxUint64 no longer fails on offset+size overflow. TestServiceWriteReadAtSaturatedFinalChunkBoundary now covers an overflowing request that returns only the remaining bytes. The boundedEnd caller audit found only Service.Read.

Comment thread kv/shard_store.go
Comment on lines +342 to +344
// Keep every captured range so backup materialization remains pinned to the
// catalog snapshot. Unclamped scan dispatch de-duplicates these by group.
return s.engine.GetIntersectingRoutes(nil, nil), true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Filter off-route usage counter copies

For StatFS, routedUsageCounterAt scans !fs|usage|route| and this branch fans the scan out to every group, but normal ScanAt does not materialize keys through the captured route or deduplicate equal keys. If a range move leaves a stale usage-counter copy on the old owner while the migrated copy exists on the new owner, both values are returned and merged, so used bytes/files are double-counted. Usage scans need the same owner filtering used by backup materialization, or stale off-route copies must be ignored before merging.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4685081. Filesystem usage scans now decode each routed counter embedded route key and retain it only when the scanned group is the current catalog owner. The filter covers forward value scans, key-only scans, and reverse scans. The regression test places both a duplicate stale value and a stale-only counter on the old group and verifies only the owner copy is returned.

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Latest head: 4685081

Resolved the current-head findings:

  • tail reads use saturating end arithmetic before EOF clamping
  • routed filesystem usage scans discard stale copies from non-owner groups across forward, key-only, and reverse scans

Verification:

  • go test ./internal/filesystem ./kv -count=1
  • go test -race ./internal/filesystem ./kv -run "TestServiceWriteReadAtSaturatedFinalChunkBoundary|TestShardStoreScanAt_RoutesFilesystemUsageCountersAcrossRouteGroups" -count=1
  • go test ./... -run "^$" -count=1
  • golangci-lint --config=.golangci.yaml run ./internal/filesystem ./kv --timeout=5m
  • git diff --check
  • caller audit for boundedEnd and every filesystem usage scan path

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46850812fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread kv/backup_scan.go
var clampToRoutes bool
if st != nil {
routes, clampToRoutes = st.routesForScan(start, end)
routes, clampToRoutes = st.routesForForwardScan(start, end)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the owner route for backed-up usage counters

When the backup range overlaps fskeys.UsageRouteAllPrefix(), routesForForwardScan now fans out to every route; if a split/ownership move leaves a stale copy of the same routed usage counter in a later non-owner group, scanKeyRoutesWithSourceAt de-duplicates equal keys to that later route and materializeRouteForKey then rejects it, so Next advances past the key without emitting the live counter from the owner group. This can make backups omit filesystem usage counters even though normal forward/key-only/reverse scans filter stale copies.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e9900d1. Duplicate routed usage keys now retain the copy from the live owner group before backup materialization. Added TestBackupScannerPrefersFilesystemUsageOwnerCopy; kv and full repository tests pass.

Comment thread kv/shard_store.go Outdated
Comment on lines +425 to +426
if filterUsageOwners {
kvs = s.filterFilesystemUsageKVsForGroup(kvs, route.GroupID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refill after filtering stale usage rows

When a usage-counter scan hits a group that has at least limit stale !fs|usage|route|... rows before an owned counter, this post-scan filter can shrink the page below the requested limit without scanning further in the same group. Callers such as StatFS page until len(page) < statFSScanPageSize, so they can stop early and miss later owned counters after route moves/splits leave many stale copies behind.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e9900d1. Forward value, forward key-only, and reverse usage scans now continue paging after owner filtering until the requested limit is refilled or the group is exhausted. Added TestShardStoreScanAt_RefillsAfterStaleFilesystemUsageCounters; kv and full repository tests pass.

Comment thread adapter/grpc.go
Comment on lines +242 to +248
if req.GetReverse() {
groupScanner, ok := r.store.(rawGroupReverseScanner)
if !ok {
return nil, errors.WithStack(status.Error(codes.FailedPrecondition, "reverse raw scan with explicit group requires a group-aware store"))
}
res, err := groupScanner.ReverseScanGroupAt(ctx, groupID, req.StartKey, req.EndKey, limit, readTS)
return res, errors.WithStack(err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Support keys-only explicit reverse scans

When a client sends RawScanAt with GroupId, Reverse, and KeysOnly, the request is routed to rawScanKeysAt before this new value-scan helper is reached, and that path still rejects explicit-group reverse scans. This leaves the newly supported explicit-group reverse scan path working only when values are requested, while key-only callers continue to get InvalidArgument for the same group/range.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e9900d1. GroupId + Reverse + KeysOnly now uses ReverseScanGroupAt and strips values without falling back to the global scan. Added TestGRPCServer_RawScanAt_ReverseKeysOnlyUsesExplicitGroup; adapter and full repository tests pass.

@bootjp bootjp changed the title Implement filesystem foundation milestones Complete filesystem mount, migration, and recovery Jul 18, 2026
@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Implemented the remaining filesystem design requirements at e9900d1.

Evidence:

  • actual go-fuse raw server plus opt-in --filesystemMount lifecycle, root initialization, startup recovery, and graceful unmount
  • durable whole-file move phases with epoch fencing, resumable OCC-conflict recovery, and source/target cleanup tests
  • durable create/delete intents and restart recovery tests
  • placement metrics, five validated Prometheus rules, and a provisioned Grafana dashboard
  • current-head regressions covered for backup usage owner selection, stale-row scan refill, and explicit-group reverse keys-only scans
  • go test ./..., modified-package tests, targeted race tests, full lint (0 issues), Linux amd64 builds, promtool check config, and dashboard JSON validation pass
  • commit signature verified for bootjp contact@bootjp.me

Kernel mount smoke remains environment-dependent and is documented for a FUSE-capable deployment host; the raw protocol and Linux compile paths are covered here.

@codex review

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

All required checks are green on e9900d1b822fe4d936245f979682e12189ae5ac6 (unit/integration, Jepsen, lint, Docker builds, and CodeQL). Current-head root inline findings remain empty.\n\n@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9900d1b82

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if refreshedMeta.Type == TypeDirectory {
return 0, nil, false, ErrIsDir
}
return refreshedTS, refreshedMeta, true, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recheck active home after stale-chunk cleanup

When a size-changing Write/Truncate passes ensureActiveHomeAt and then MoveFile flips the file to MIGRATING while deleteChunksPastCurrentEOF runs, this refresh can load the migrating inode and return it as usable. The later transaction only includes HomeKey as an OCC read, so because the migration state already existed at refreshedTS it will not conflict; a grow/truncate can then commit during the move and the target copy can miss or retain stale chunks. Revalidate the refreshed home at this timestamp before returning it to callers.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9be920ff. cleanupStaleChunksBeforeSizeChange now revalidates ensureActiveHomeAt at the refreshed timestamp after stale-chunk cleanup, so Write/Truncate/SetAttr fail with ErrStaleHome if migration started during maintenance. Added TestServiceTruncateRejectsHomeThatStartsMigratingDuringStaleCleanup and covered it under targeted race tests.

if index+1 < len(result.Entries) {
handle.positions[offset] = dirPosition{cookie: position.cookie, skip: index + 1}
} else {
handle.positions[offset] = dirPosition{cookie: result.NextCookie}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop readdir from restarting at EOF

When the backend returns the final directory page (NextCookie == ""), this records the empty start cookie for the last offset. The kernel will use that offset for the next READDIR call, so ReadDir calls the backend with an empty cookie and starts listing from the beginning again, producing duplicate entries and potentially an endless ls on ordinary directories whose final page fits in the buffer. Store an EOF state for that offset instead of the empty cookie.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9be920ff. Directory offsets now record a terminal EOF state when NextCookie is empty, and a subsequent READDIR at that offset returns an empty page without calling the backend again. Added TestRawFileSystemReadDirKeepsEOFOffsetTerminal.

elems, reads, delta, err := s.rmdirTargetTxnParts(ctx, ts, parentMeta, replacedMeta)
return elems, reads, delta, nil, err
}
return s.unlinkFileTxnParts(ctx, ts, replacedMeta, now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject replacing a migrating file

When a same-directory rename overwrites a file whose home is already MIGRATING, this path calls unlinkFileTxnParts without the active-home guard that direct Unlink uses. Because the rename reads the already-migrating inode at its ts, the transaction can commit, mark/delete the target while the move job is in progress, and then the mover hits ErrMoveFenceLost with durable job/intent state and leftover chunks; check ensureActiveHomeAt for the replacement before unlinking it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9be920ff. Replacement-file rename now requires an ACTIVE matching home before building unlink parts, and unlink transaction paths retain HomeKey as an OCC read so a move starting before commit also fences the namespace mutation. Added TestServiceRenameRejectsMigratingReplacement.

}
for _, pair := range page {
if pair != nil && bytes.HasPrefix(pair.Key, prefix) {
out = append(out, pair)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the recovery scan limit

When startup or an operator calls RecoverIntents with the default/explicit limit, scanRecoveryPrefix still keeps fetching limit-sized pages until the prefix is exhausted because every page is appended here without a total cap. On nodes with many stale intents or move jobs this defeats the intended bounded recovery batch and can make FUSE mount startup spend the full recovery timeout processing all records; stop once len(out) == limit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9be920ff. scanRecoveryPrefix now returns as soon as the accumulated visible record count reaches the requested limit. TestServiceRecoverIntentsHonorsScanLimit verifies a three-record backlog is processed as bounded 2+1 batches.

Comment on lines +123 to +127
result, errno := r.adapter.Create(ctx, input.NodeId, []byte(name), filesystem.CreateOptions{
Mode: input.Mode & permissionModeMask,
UID: input.Uid,
GID: input.Gid,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid opening files for MKNOD

For kernel MKNOD, this goes through Adapter.Create, which always injects the mount client ID and creates a durable open-handle lease, then performs a separate Release. If that release is canceled or hits a transient conflict after the create transaction committed, the syscall returns an error even though the file now exists, and a retry sees EEXIST while the leaked lease remains until the reaper runs. Use a create path that does not allocate an open handle for MKNOD.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9be920ff. Kernel MKNOD now uses a dedicated adapter path that clears ClientID before the core Create call, so no durable open handle or follow-up Release is created. Added TestRawFileSystemMknodDoesNotAllocateOpenHandle.

if !ok {
return nil
}
groupID, found := resolver.FilesystemGroupForHome(home, inode)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use scanned shard owner for placement chunk groups

When a stale physical copy leaves chunks for the same (home,inode) on another shard group that is still scanned by the chunk-prefix placement pass, resolving each chunk through the current catalog here attributes both copies to the same logical owner. That keeps len(groups) at 1, so MultiShardFiles and the critical alert miss the exact physical split they are meant to detect; carry the source group from the scan instead of recomputing it from the key.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9be920ff. Placement collection now enumerates every physical ShardStore group and uses explicit-group scans, attributing each chunk to the scanned source group instead of the current route owner. Added same-key cross-group stale-copy coverage and sorted physical-group enumeration coverage.

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Addressed all six latest-head findings on 9be920fff56ee208b21444e22aa761474b705059.

Changes:

  • revalidate ACTIVE home after stale-chunk cleanup and retain HomeKey OCC reads for unlink/rename replacement fencing
  • keep final FUSE readdir offsets terminal at backend EOF
  • create MKNOD files without durable open-handle leases
  • enforce the configured recovery scan limit
  • attribute placement chunks to explicit physical source groups
  • update the filesystem design document and add focused regressions

Caller audit:

  • all three cleanupStaleChunksBeforeSizeChange callers (Write, Truncate, SetAttr size) propagate ErrStaleHome; FUSE maps it to EAGAIN
  • renameTargetTxnParts is only used by same-directory Rename
  • MKNOD has one raw-FUSE caller and does not change CREATE semantics
  • recovery scan callers are move-job and intent recovery only
  • physical group enumeration is consumed only by placement collection

Validation:

  • go test ./internal/filesystem/... ./kv -count=1 -timeout=600s
  • targeted race tests for all six findings
  • golangci-lint --config=.golangci.yaml run ./... --timeout=10m (0 issues)
  • go test ./... -run=^$ -count=1 -timeout=600s
  • Linux amd64 FUSE test binary and server builds
  • git diff --check and signed commit verification

Local full-suite note: go test ./... -count=1 -timeout=900s passed all touched packages but the unrelated adapter/Raft integration package failed after about 594s while repeatedly dialing stopped localhost peers. The pushed CI test and Jepsen jobs are the final integration signal.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9be920fff5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/filesystem/migration.go Outdated
Comment on lines +481 to +483
meta, err := s.inodeAt(ctx, job.Inode, ts)
if err != nil {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tolerate unlink while cleaning moved source

When a move has reached MovePhaseSourceCleanup, switchMoveHome has already made the target home ACTIVE, so a concurrent Unlink can legally remove the file before ResumeMoveFile deletes the old source chunks. In that case this inodeAt/homeAt validation returns ErrNotFound and the move job/intent remains incomplete, leaving job.SourceHome chunks orphaned and still visible to physical scans/backup. Source cleanup needs to continue from the durable job state, or otherwise finish the job, when the inode/home disappeared after the switch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3b207a5. Source cleanup now permits the precise post-switch unlink state where both inode and home are absent, while every other missing or mismatched fence still fails. The durable job source prefix is then paged to completion. Added TestServiceResumeMoveSourceCleanupAfterConcurrentUnlink and targeted race coverage.

opts := defaultMountOptions(options)
if recoverer, ok := adapter.core.(intentRecoverer); ok {
recoveryCtx, cancel := context.WithTimeout(context.Background(), startupRecoveryTimeout)
_, recoveryErr := recoverer.RecoverIntents(recoveryCtx, 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Drain all recovery batches before mounting

When more than defaultIntentRecoveryLimit unfinished move jobs survive a restart, this single RecoverIntents(..., 0) call processes only the first bounded batch and then exposes the mount; the remaining jobs stay in their migrating phases, and there is no later production caller here to resume them, so affected files can keep returning EAGAIN for writes/unlinks indefinitely. Startup should keep draining bounded batches until no recovery work remains, or schedule recovery before serving FUSE requests.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3b207a5. FUSE startup now drains bounded 256-record recovery batches until a batch reports no resumed jobs, cleared completed jobs, or cleared intents. Recovery also removes completed move-job records so an earlier page cannot hide later unfinished work. Added service pagination and mount-drain regressions.

Comment thread main.go
return err
}
installFilesystemServerLifecycle(ctx, eg, cleanup, server, serveDone, config.mountPoint)
startFilesystemPlacementCollector(ctx, eg, service, *filesystemPlacementScanInterval)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Schedule the open-handle lease reaper

With the FUSE mount enabled, this starts only the placement collector; no production lifecycle path schedules ReapExpiredOpenHandleLeases. If a client dies or a mount is lost after unlinking an open file, the durable !fs|ref lease eventually expires but is never removed, so the orphaned inode/chunks and their StatFS usage remain indefinitely. Start a periodic lease reaper alongside the placement collector so expired handles actually get reclaimed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3b207a5. The mounted filesystem lifecycle now starts a periodic expired open-handle lease reaper, enabled by default at 30 seconds and stopped through the shared context. It logs reclaimed refs/orphans and has a periodic lifecycle regression test.

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Addressed all three latest-head findings on 3b207a5.

Changes:

  • allow durable source cleanup to finish when a post-switch unlink has removed both inode and home
  • drain bounded recovery batches before exposing the FUSE mount and reclaim completed move-job records between batches
  • run the expired open-handle lease reaper periodically in the mounted-server lifecycle
  • update the filesystem design document and add focused regressions

Caller audit:

  • validateMoveState is used only by migration copy and cleanup; missing records are accepted only during source_cleanup/completed when both inode and home are absent
  • moveJob consumers are internal migration/recovery paths and tests; no external API or placement metric requires retained completed recovery records
  • RecoverIntents production use is the pre-mount drain loop
  • ReapExpiredOpenHandleLeases retains its existing direct tests and is now also scheduled from the FUSE lifecycle

Validation:

  • go test ./internal/filesystem/... . -count=1 -timeout=600s
  • targeted race tests for source-cleanup unlink, multi-batch recovery, mount drain, and periodic lease reaping
  • golangci-lint --config=.golangci.yaml run ./... --timeout=10m (0 issues)
  • go test ./... -run=^$ -count=1 -timeout=600s
  • Linux amd64 FUSE test binary and server builds
  • git diff --check and signed commit verification

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b207a5c2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +219 to +223
if completed {
if s.observer != nil {
s.observer.ObserveMoveJob(jobID, false)
}
return job, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear completed move jobs before returning

When MoveFile/ResumeMoveFile completes during normal operation, this path returns while leaving the completed !fs|job|move|... record that was just written by the source-cleanup step. Since completed jobs are only cleared by RecoverIntents during startup recovery, a long-lived mounted server that performs successful migrations accumulates these rows indefinitely, and placement/recovery scans keep paying to decode them even though no consumer needs retained completed records. Clear the move job on this completion path before returning.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 63b15cd. ResumeMoveFile now deletes the completed durable job before returning, so normal migrations no longer accumulate completed rows. Recovery still clears legacy completed records. TestServiceMoveFileCopiesSwitchesAndCleansSource now asserts the job is absent.

Comment on lines +207 to +209
if s.observer != nil {
s.observer.ObserveMoveJob(jobID, true)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Clear active move gauges when resume fails

If ResumeMoveFile returns before completion — for example a stale/missing job ID from an internal retry path, context cancellation, or a terminal fence error — this active marker is never removed from FileSystemMetrics.activeMoveJobs. Because updateMoveInflightLocked takes the max of durable scan results and the active map, one failed resume can keep elastickv_fs_file_move_inflight elevated until process restart even after no durable job remains. Use a cleanup path that emits ObserveMoveJob(jobID, false) on all exits where the resume loop is no longer running.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 63b15cd. Move observation cleanup is deferred immediately after the active marker is set, covering missing jobs, cancellation, fence errors, clear failures, and successful completion. Added TestServiceResumeMoveFileClearsActiveObservationOnFailure.

Comment on lines +128 to +130
if job.Phase != MovePhaseCompleted {
if _, resumeErr := s.ResumeMoveFile(ctx, job.ID); resumeErr != nil {
return stats, resumeErr

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tolerate concurrently cleared recovery jobs

When two filesystem-enabled nodes run the pre-mount recovery drain at the same time, both can scan the same non-completed move job; if one node finishes the resume and clears the job before the other reaches this call, ResumeMoveFile returns ErrMoveJobNotFound and this path aborts that node's mount even though recovery already succeeded. Treat a missing job during startup recovery as already handled so concurrent drainers are idempotent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 63b15cd. Startup recovery treats ErrMoveJobNotFound from ResumeMoveFile as already completed by another drainer, and recovery-record deletion retries OCC conflicts. Added a barrier-backed concurrent two-drainer race regression.

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Addressed all three latest-head findings on 63b15cd.

Changes:

  • delete completed move-job records before normal ResumeMoveFile success returns
  • clear active move observations on every ResumeMoveFile exit
  • tolerate jobs concurrently completed and cleared by another startup drainer
  • retry recovery-record OCC conflicts and add focused concurrency regressions
  • update the filesystem design document

Caller audit:

  • ResumeMoveFile callers are MoveFile and the two recovery paths; none requires retained completed records
  • placement metrics count unfinished durable jobs and remain correct after immediate completed-record deletion
  • ObserveMoveJob has one production caller and now receives balanced true/false events on every return path
  • clearMoveJob and clearRecoveryRecord are internal recovery/migration cleanup only

Validation:

  • go test ./internal/filesystem/... -count=1 -timeout=600s
  • targeted race tests for normal completion cleanup, failed-resume observation cleanup, completed-page recovery, and concurrent recovery drainers
  • golangci-lint --config=.golangci.yaml run ./... --timeout=10m (0 issues)
  • go test ./... -run=^$ -count=1 -timeout=600s
  • Linux amd64 server build
  • git diff --check and signed commit verification

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

https://github.com/bootjp/elastickv/blob/63b15cddce2d5951bd318c6b4aedb7793d101ea4//tmp/elastickv-review/internal/filesystem/service.go#L375-L381
P2 Badge Make root initialization idempotent under races

When two filesystem-enabled servers bootstrap an empty store at the same time, both can pass the missing-root check using the same snapshot; one transaction creates the root and the other returns the wrapped store.ErrWriteConflict here, causing startFilesystemIfEnabled to abort even though the desired root now exists. Please handle this conflict by re-reading or retrying so concurrent startup remains idempotent.


https://github.com/bootjp/elastickv/blob/63b15cddce2d5951bd318c6b4aedb7793d101ea4//tmp/elastickv-review/internal/filesystem/migration.go#L131-L133
P2 Badge Block overlapping moves before cleanup completes

After a move switches the home but before its source-cleanup phase finishes, this active-home check allows a second MoveFile for the same inode to start. That second move bumps the inode/home epoch, so the first job later fails switchedMoveFenceMatches in recovery and leaves its old source chunks and durable job stuck; please also fence on unfinished move jobs for the inode or make source cleanup tolerate newer active homes.


https://github.com/bootjp/elastickv/blob/63b15cddce2d5951bd318c6b4aedb7793d101ea4//tmp/elastickv-review/internal/filesystem/service.go#L840
P2 Badge Use an uncanceled context for post-commit shrink cleanup

When a shrink touches more than the first chunk-delete page, the transaction above has already committed the smaller file size and partial usage delta before this paged cleanup runs with the caller's context. If the FUSE/client request is canceled or hits its deadline after the commit, the remaining beyond-EOF chunks and their usage accounting are left behind, and no background reaper scans live files for this state; please run the committed cleanup with an uncanceled/best-effort context or make it recoverable.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Addressed the three latest-head P2 findings on 99380f9.

Changes:

  • retry root initialization after OCC conflicts so simultaneous empty-store mounts converge on the winning atomic root transaction
  • reject a second MoveFile while any unfinished durable move job exists for the inode, including source cleanup after the home switch
  • run all committed chunk-delete plans with a context that no longer inherits request cancellation
  • update the filesystem design document and add focused regressions

Caller audit:

  • InitializeRoot has one production caller in the FUSE mount startup path
  • activeMoveSource and the unfinished-job scan are used only by MoveFile
  • finishCommittedChunkDeletes is used only after successful SetAttr, Truncate, Release, lease-reaper, rename, or unlink transactions
  • deleteChunkPagesAndFinalize remains the internal cleanup primitive and receives the derived cleanup context

Validation:

  • go test ./internal/filesystem/... -count=1 -timeout=600s
  • targeted race tests for concurrent root initialization, overlapping source-cleanup migration, and Truncate/SetAttr cancellation after commit
  • golangci-lint --config=.golangci.yaml run ./... --timeout=10m (0 issues)
  • go test ./... -run=^$ -count=1 -timeout=600s
  • Linux amd64 server build
  • git diff --check and signed commit verification

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 99380f9a4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bootjp
bootjp merged commit 2dc4106 into main Jul 19, 2026
10 checks passed
@bootjp
bootjp deleted the design/filesystem-elastickv-foundation branch July 19, 2026 06:00
bootjp added a commit that referenced this pull request Jul 20, 2026
## Summary

- rename the completed filesystem design document from `partial` to
`implemented`
- align the filename with its existing implemented header and the
design-document status convention

## Verification

- confirmed PR #996 is merged with all required checks passing
- confirmed no references remain to the old filename
- pre-commit golangci-lint completed successfully

Author: bootjp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant