Skip to content

GO-7348 spacev2: greenfield space orchestration rewrite - #3190

Draft
requilence wants to merge 25 commits into
developfrom
go-7348-spacecontroller-refactor
Draft

GO-7348 spacev2: greenfield space orchestration rewrite#3190
requilence wants to merge 25 commits into
developfrom
go-7348-spacecontroller-refactor

Conversation

@requilence

Copy link
Copy Markdown
Contributor

Summary

  • Replaces the space orchestration layer with a new per-space reconciler design in space/spacev2/ (design rationale in space/spacev2/DESIGN.md, build log in space/spacev2/HANDOFF.md)
  • Reuses the layers that own on-wire/on-disk/crypto formats as-is: clientspace, techspace, spacecore, spaceinfo, storage/migration — only orchestration (lifecycle, state machine, watcher, registry, lazy-load/pause/unload) is rewritten
  • Cutover complete: v1 orchestration removed (service, load, create, join, waiter, watcher, dedupqueue, spacefactory, the five controller types, spaceprocess, spaceoffloader, syncstopper). space is now a thin façade preserving the v1 Service interface (so existing space/mock_space consumers need no changes); spacev2 owns client.space
  • Net effect: 57 files changed, +6951/−7393 lines

Base branch note

Stacked on #3081 (GO-6758) since this branch was cut before that PR merged to develop. Retarget to develop once #3081 lands.

Status

Draft — all unit suites are green (go build ./..., go vet ./..., go test ./space/..., plus consumer suites), including -race stress runs on the reconciler's seq-freshness logic. Not yet done: real end-to-end runtime verification (existing accounts of all vintages loading/syncing, cross-device profile + push, lazy mode against a real client) — flipping out of draft once that's verified.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./space/... (incl. new spacev2 unit + race-stress tests)
  • Runtime verification with real accounts (new / migrated / joined / streamable / one-to-one)

KirillSto and others added 25 commits April 8, 2026 12:01
Replace single GetMessages call that loads all messages into memory
with batched iteration (100 messages per batch). Add OrderAsc field
to GetMessagesRequest for forward pagination. Extract shared
chatMessageToSearchDoc helper to eliminate duplication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove unnecessary OrderAsc field from GetMessagesRequest. Paginate
using existing BeforeOrderId with batch[0].OrderId as cursor, since
queryMessages always returns results sorted ascending.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
docs/SpaceController.md is the clean-room reimplementation spec; the
spacev2 package is the inert skeleton (not registered in bootstrap).
…te path)

- addStatic no longer double-closes the ready channel of a failed entry
  (found by writing the steady-state test)
- new tests: ensure-vs-closeAll shutdown race; apply drives Update on an
  existing controller; SpaceView detail change flows end-to-end through
  the subscription UpdateKeys path; convertSpaceError contract
Port of v1 mode.StateMachine with two sanctioned changes: waiters receive
the start-failure cause (v1 TODO [MR] — waiters got nil), and CanTransition
is dropped (§11.3 — gating lives in the controller mode mapping; offloading
stays non-terminal for CancelLeave). Same-mode requests during a divergent
in-flight transition now get ErrTransitionInProcess instead of a handle to
the process being torn down.
Remove the M1-M3 orchestration implementation (service, controller, state
machine, registry, watcher, factory, tech provider, marketplace) and its
tests, leaving only HANDOFF.md. Per the greenfield mandate the next
implementer starts fresh from the docs, not the existing shape.

Full code preserved on branch spacev2-agent-wip
(recover: git checkout spacev2-agent-wip -- space/spacev2).
Greenfield restart per HANDOFF.md: replace the mode-state-machine +
five-controller-types shape with a per-space reconciler. DESIGN.md is the
normative architecture doc.

- state.go: State/Target enums, pure decide(status, wanted) -> target,
  Fatal() error marker for non-retryable backend failures.
- controller.go: one reconcile goroutine per space converges current state
  to the target through a narrow Backend seam (AccountStatus/Load/Unload/
  Offload/Join); controller owns retry with exponential backoff; input/
  decision sequence numbers make WaitLoaded's terminal answers race-free
  against concurrent input changes and prevent stale-poke re-attempts.
  Pause/unload is a first-class Loaded->Idle transition.
- registry.go: idempotent getOrCreate keyed by spaceId, no waiting map, no
  error caching; concurrent close-all.
- Unit tests (fake backend with step-concurrency detector) cover load/
  pause/delete/restore/join-accept/join-reject/offload-without-load,
  transient retry, fatal park+surface+recover, mid-load status flips,
  shutdown during load, waiter semantics; 10x -race clean.

Claude-Session: https://claude.ai/code/session_01AHPvaSRimgrQmY2BWpomsU
Backend implementation for the reconciler (backends.go, presetloader.go):

- Load: guest-key decode, optimistic-Ok LocalStatus publication,
  clientspace.BuildSpace via spacecore, access-type write, mandatory-objects
  + ACL-head gates, then the reused post-load domain components
  (aclobjectmanager, participantwatcher, aclnotifications, migration,
  personalmigration for the personal space) hosted in a child app; the
  presetLoader shim satisfies their spaceloader.SpaceLoader barrier with the
  already-built space, so OnSpaceLoad/OnSpaceUnload keep firing from
  aclobjectmanager exactly as in v1. Terminal build failures map to the
  same LocalStatus/RemoteStatus writes as v1's spaceloader and are Fatal;
  transient ones keep Loading and are retried by the controller.
- Unload (pause): close pipeline then space; ocache eviction stops sync;
  keeps on-disk data and LocalStatus.
- Offload: AddSpaceToDelete always; skip when already Missing; storage
  (10s lock timeout) -> files -> indexes -> LocalStatusMissing; missing
  storage tolerated (idempotent).
- Join: participant-status + LocalStatusUnknown writes, then hosts the
  any-sync aclwaiter until accept (Active+head) / reject (Deleted+head +
  decline notification), blocking as the reconciler's join step.

Also fixes a controller determinism hole the new tests exposed: the demand
flag is now snapshotted together with inputSeq, so a SetWanted racing the
status read can no longer produce a fatal outcome stamped stale (which was
silently re-attempted). Backend unit tests use embed-nil fakes for the
SpaceView/storage/indexer/offloader seams; 30x -race suite runs clean.

Claude-Session: https://claude.ai/code/session_01AHPvaSRimgrQmY2BWpomsU
- bootstrap.go: tech-space resolution as an explicit, pure decision tree
  (new-account create / load / offline-old-account fallbacks / nodes-report-
  missing), isolated from v1's nested initAccount; real create/load impls
  over spacecore + clientspace.NewTechSpace; ensurePersonalSpace heals a
  missing personal SpaceView on every start (subsumes v1's SkipCheckSpaceView
  path); marketplace becomes a static virtual-space entry with reindex-once
  (a failed reindex now keeps reporting its error instead of v1's silent
  success on the second call).
- watcher.go: SpaceView objectsubscription -> onEvent; no dedupqueue — the
  controller's buffered poke coalesces bursts; initial snapshot replayed via
  Iterate (OnAdded only fires for later additions).
- service.go: component skeleton — Init (deps, deterministic ids, account
  metadata), Run (marketplace -> resolve tech space -> register on child app
  -> techSpaceReady -> personal ensure -> watcher -> new-account synchronous
  personal load -> lazy drain -> StartSync -> persist network id), Close
  (watcher -> controllers -> tech space). Lazy mode is just wanted=false on
  discovery: no deferredStatuses cache; drain is a SetWanted sweep.
- controller.go: close the second stale-decision hole the stress runs
  caught — SetWanted now bumps inputSeq in the same critical section as the
  wanted write (previously a separate Poke could let the loop snapshot new
  demand with an old seq stamp and re-attempt a fatal load). 75x -race
  suite runs clean.

Claude-Session: https://claude.ai/code/session_01AHPvaSRimgrQmY2BWpomsU
- api.go: full verb surface on the reconciler primitives — Get (controller
  must exist; promotes paused/deferred spaces), Wait (creates the controller
  directly once the SpaceView exists; no 500ms polling), Create/CreateOneToOne
  (single build path: core-space create -> MarkSpaceCreated -> SpaceViewCreate
  -> WaitLoaded; one-to-one revives a locally-removed view), Join/InviteJoin
  (unidirectional: write Joining/Active + acl head, reconciler does the rest),
  CancelLeave/Delete (status writes), AddStreamable (idempotent), accessors,
  PreloadRemainingSpaces, UpdateRemoteStatus/UpdateSharedLimits/AllSpaceIds
  (deletioncontroller.SpaceManager, compile-time asserted), AllLoadedSpaceIds
  + SyncAllSpaceHeads (GO-7302 foreground kick), participant-remove
  notification, stream auto-join with backoff, documented error set.
- service.go: watcher policy — remote-deleted reaction (notify + flip
  AccountStatusDeleted, no controller build, mirroring v1's early return),
  lazy-mode preferred-broken fallback, computeLazyMode with view validation.
- bootstrap fix: v1 createAccount never creates the derived personal space —
  a new account gets the tech space plus one *created* first space
  (firstCreatedSpaceId). resolveTechSpace now reports the old-account path,
  which is the only place the personal SpaceView is healed eagerly.
- Tests: API fixture over scripted techspace/notification fakes covering all
  verbs, discovery policies, remote-deleted reaction, preload drain,
  preferred-broken release; suite -race clean.

Claude-Session: https://claude.ai/code/session_01AHPvaSRimgrQmY2BWpomsU
Package space becomes a thin facade: the v1 Service interface verbatim (all
space/mock_space users keep working without regeneration), the error set
aliased to spacev2's values (errors.Is works across both import paths, incl.
the historical ErrSpaceStorageMissig spelling), and New() returning the v2
service. spacev2 takes over CName client.space; the SpaceView watcher
subscription id therefore matches v1's.

Removed v1 orchestration: service/load/create/join/init/metadata/waiter/
streamable/spacesub/spacewatcher + tests, dedupqueue, spacefactory, the five
controller types (personal/shareable/streamable/marketplace + generic),
spaceprocess (mode state machine, loader/offloader/joiner/initial) and the
v1-only components spaceoffloader + syncstopper (dead code). The reused
pipeline components stay under space/internal/ (spacestatus, spaceloader +
builder as interface seams for them, aclobjectmanager, participantwatcher,
aclnotifications, migration, personalmigration, dependencies, objectprovider).

core/anytype/bootstrap.go: drop the spacefactory registration; space.New()
now boots v2. .mockery.yaml: drop entries for deleted packages.

Verified: go build ./... green; go vet over space+core (tests compile);
test suites pass for ./space/..., ./core/anytype/..., ./core/block/...,
./core/acl/..., ./core/indexer/..., ./core/subscription/... Runtime
verification against real accounts is the remaining step (HANDOFF updated).

Claude-Session: https://claude.ai/code/session_01AHPvaSRimgrQmY2BWpomsU
Review finding (confirmed independently by 4 lenses + 8 adversarial
verifiers): v1's lazy mode collapsed to eager after the first backlog
release (the releasing flag in decideAndApplySpaceStatus), but v2's
wantedOnDiscovery kept returning false for non-preferred spaces forever.
A SpaceView first discovered after the drain — a space shared to this
account or created on another device mid-session — was created with
wanted=false and never background-loaded or synced until manually opened.

Fix: a lazyReleased atomic flag set by drainDeferredLater BEFORE the
SetWanted sweep (an event racing the sweep either sees the flag or its
controller is already registered and swept — no gap), checked by
wantedOnDiscovery. Also covers CancelLeave of a non-preferred space after
the drain. Regression test pins the late-discovery eager behavior.

Claude-Session: https://claude.ai/code/session_01AHPvaSRimgrQmY2BWpomsU
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

New Coverage 48.9% of statements
Patch Coverage 51.9% of changed statements (500/963)

Coverage provided by https://github.com/seriousben/go-patch-cover-action

Base automatically changed from go-6758-optimize-full-chat-messages-reindex to develop July 3, 2026 20:58
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.

2 participants