mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-10-30 21:09:14 -04:00
* Networking: serve streams on connections a protocol dials
The connection cache is shared between the dial side and the accept side,
so once a protocol dials a peer, the peer's own send path finds that
connection and opens its streams on it. Nothing read those streams: only
connections the event loop accepted, or those the reconnect loop handed
over, had a stream handler. The peer's request then sat unanswered until
its 30 s timeout while requests in the other direction answered in
milliseconds. This is the reverse RegisterDeviceRequest timeout SPAC-12
saw after join-remote.
get_or_create_connection now hands every connection it dials to the
event loop as TrackOutboundConnection, which spawns the same stream
handler an inbound connection gets. Messaging, sync, and job activity
pass their event loop handle; pairing keeps its own connection handling.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Sync: the device that joins a library is the one that backfills
Every sync service started Uninitialized and backfilled from the first
connected partner, whatever state it already held. When the Mac Studio
joined the MacBook's library the MacBook was the one with a partner in
its device table, so it went Uninitialized -> Backfilling{Studio} and
applied the empty copy's snapshot over its own rows (LWW on the
deterministic default-space UUID). The Studio never backfilled because
the reverse registration had timed out and its device table held only
itself.
A copy of a library created from another device's library is the only
thing that may start Uninitialized. create_library_with_id now writes an
initial_backfill_pending marker into sync.db; PeerSync reads it on open
and starts Ready otherwise. A Ready device never requests a snapshot
(SharedChangeRequest.include_snapshot = false on catch-up) and repeated
catch-up failures no longer escalate to a full backfill. A copy still
waiting for its own first backfill refuses to serve as a join target.
The join carries the rule explicitly: RegisterDeviceRequest has
needs_initial_state, and RegisterDeviceResponse returns the responder's
own device record so the joiner's table is complete when the command
returns, replacing the spawned reverse request. sync_setup fails
instead of reporting devices_registered: true when that record is
missing.
Catch-up completion is recorded per peer in sync.db even when no rows
arrived; the Ready loop catches up once with a peer it has no record
for and no longer re-polls every peer whose watermark is 60 s old. A
missing watermark means never synced, not ancient, so the forced full
sync warning only fires for a real watermark past retention.
library_join_test pairs two daemons for real, joins Bob to Alice's
library, and asserts Alice's rows are unchanged, Bob holds Alice's
device row and spaces, and Alice runs no further catch-ups.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Sync: test that catch-up with an empty peer runs once
Alice holds a volume, Bob holds nothing of his own, so Alice never
writes a per-resource watermark for Bob. Before the per-peer catch-up
record, that made Bob a new peer on every loop iteration and Alice
caught up with him forever (2 -> 5 sessions in 16 s on 5e285d3). The
test asserts the session count stops moving and the record exists.
Both new suites join the xtask integration list.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Library info: fleet totals count the same devices as device_count
add_device_summaries added every cached peer summary to a library's
statistics, whatever library the peer belonged to, and the summaries
carried no source count. A joined device therefore reported the member's
3 million files next to Sources 0 and Devices 1.
Summaries now carry the device's own source_count, and only summaries
of devices in the library's device table are added, so files, content,
sources, and capacity describe the members that device_count counts.
The CLI labels the block "Statistics (all library members)".
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Docs: describe the pairing and library join flow as it runs
pairing.mdx and devices.mdx described a confirmation prompt that does
not exist and pointed at `sd devices list` for pairing status, which
reads the library's device table and does not show a peer until it
joins the library. The CLI overview lacked the global `--device` flag,
and the sync setup walkthrough never named `library sync-setup
--action join-remote` or `--node-id`. All four now describe the flow
SPAC-12 ran, and library-sync.mdx gains sections on joining, catch-up,
and fleet totals. One PROJECT_STATUS register row.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Sync: catch up once per connection session, never serve a snapshot while pending
Review of #3110 found two gaps. The per-peer catch-up record was never
cleared, so a peer that changed rows while disconnected was never caught
up with again after reconnecting (the live broadcast only lives in the
in-memory retry queue). The record is now cleared when the sync service
starts and when the peer's connection is lost; ConnectionEstablished is
not used because the event loop emits it per accepted stream.
The SharedChangeRequest handler attached a full snapshot without
checking the first-backfill marker, so two copies that both joined from
the same member could seed each other. It now refuses the snapshot while
pending, and the register handler reads the marker from sync.db directly
when the library's sync service is not up yet.
Also: a failed backfill or catch-up left active_backfill_sessions
incremented; the mock transport no longer lists a peer as a partner
before its sync service is registered; the catch-up test now starts
Alice Ready (marker cleared) and Bob pending, the shape of a real join,
and asserts exactly one catch-up and one backfill.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Sync: move past a peer that refuses a snapshot, fail the guard closed, refresh stale catch-ups
A joiner whose chosen peer refused the snapshot kept choosing it: every
partner was scored has_complete_state: true and RTT is only recorded on
success, so the order never changed. A peer whose backfill failed now
scores without complete state until a backfill succeeds, so a connected
member holding the library is tried next.
The snapshot guard read an unreadable marker as "holds state"; it now
refuses, since a wrong refusal only delays a join.
A broadcast whose retries ran out while the QUIC session stayed up had
no recovery under the once-per-session rule. The Ready loop refreshes a
catch-up record older than ten minutes; with per-peer watermarks an
empty refresh is two small requests.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
---------
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
199 lines
8.3 KiB
Plaintext
199 lines
8.3 KiB
Plaintext
---
|
|
title: Library Sync
|
|
description: How devices share library state without central custody
|
|
sidebarTitle: Library Sync
|
|
---
|
|
|
|
Library sync keeps a person's Spacedrive environment coherent across paired
|
|
devices. It synchronizes small durable state, source availability, and user
|
|
assertions. It does not place every filesystem record into one replicated
|
|
library database.
|
|
|
|
That boundary lets a library grow across large NAS datasets, removable drives,
|
|
and cloud sources without making each client copy the full index.
|
|
|
|
## What library sync carries
|
|
|
|
Library sync carries state that defines the shared environment:
|
|
|
|
- paired device membership and revocation;
|
|
- source descriptors and availability;
|
|
- spaces, pins, and library presentation;
|
|
- tags, notes, policies, permissions, and review decisions;
|
|
- source subscriptions and cache intent;
|
|
- logical clocks and acknowledgements.
|
|
|
|
Large source generations follow the source replication protocol or remain
|
|
served by their owning device. File bytes move only through remote reads,
|
|
caching, file operations, or explicit file sync.
|
|
|
|
## Three planes
|
|
|
|
| Plane | Data | Direction |
|
|
| --- | --- | --- |
|
|
| Library | Devices, registry, policy, and small shared state | Merge between authorized peers |
|
|
| Source | Records, facets, content evidence, and search projection | From the ingest authority to subscribers |
|
|
| Bytes | File content and ranges | On demand or through explicit policy |
|
|
|
|
Separating these planes prevents a metadata edit from becoming a file transfer.
|
|
It also prevents one unavailable source from blocking unrelated library work.
|
|
|
|
## Pairing and trust
|
|
|
|
Pairing creates a direct trust relationship. One device generates a short
|
|
pairing secret. The joining device proves possession, the two nodes exchange
|
|
identities, and both record the relationship.
|
|
|
|
Traffic is encrypted between peers. Local discovery handles nearby devices.
|
|
The internet path attempts a direct connection and uses a relay when necessary.
|
|
Relays help packets travel. They do not become the authority for the library or
|
|
receive permission to browse it.
|
|
|
|
Device revocation is shared library state. A revoked device loses future access
|
|
to remote queries and bytes. Revocation cannot erase data that the device was
|
|
previously allowed to cache, so access policy should expose the smallest useful
|
|
scope.
|
|
|
|
## Joining an existing library
|
|
|
|
Pairing makes two devices trust each other; it does not put the new device in
|
|
any library. A device joins a library with `library sync-setup --action
|
|
join-remote` on the joiner, or `--action share-local` on the device that holds
|
|
the library. Either way, one side already has the library's state and the other
|
|
side has none.
|
|
|
|
The side that creates its copy during the join is the one that must backfill.
|
|
It persists a "first backfill pending" marker in its `sync.db` when the library
|
|
is created, starts its sync service in the `Uninitialized` state, and pulls a
|
|
state snapshot from the existing member. The join request also carries an
|
|
explicit `needs_initial_state` flag, so the existing member knows the joiner has
|
|
nothing to offer yet and does not catch up from it until the joiner reports
|
|
its first watermarks. A device that created its library itself, or that has
|
|
completed its first backfill, starts `Ready` and never adopts a full snapshot
|
|
from a peer; it only receives incremental changes and the device-owned rows of
|
|
peers it has not synced with before. A device that has not finished its first
|
|
backfill refuses state requests, so its half-built copy is never a source.
|
|
|
|
Both devices register each other in their device tables as part of the join:
|
|
the joiner sends its record with the request and the existing member returns
|
|
its own in the response. The command reports `devices_registered: false` and
|
|
`success: false` when the remote device did not register the joiner, instead
|
|
of claiming success.
|
|
|
|
## Catch-up with a peer
|
|
|
|
A `Ready` device catches up with a connected peer once per connection session.
|
|
After a catch-up completes it records the completion per peer in `sync.db`,
|
|
even when the peer had no rows to send, so the same peer is not caught up again
|
|
while the connection lasts. The record is cleared when the peer disconnects and
|
|
when the sync service starts, so changes a peer made while the two were apart
|
|
are pulled once they reconnect. Live changes travel as broadcasts and do not
|
|
depend on this loop; a record older than ten minutes is refreshed as a net for
|
|
a broadcast whose retries ran out while the connection stayed up.
|
|
|
|
## Library totals
|
|
|
|
`library info` reports fleet totals. Each member publishes its own file, unique
|
|
content, source, and capacity counts; a device adds the published figures of
|
|
every other member of that library to its own at read time. The device count is
|
|
the number of rows in the library's device table. A joined device with no
|
|
sources of its own therefore shows the same totals as the member that holds the
|
|
data, and the figures stay consistent with the device count because only
|
|
members contribute.
|
|
|
|
## Source generations
|
|
|
|
Each source generation has one ingest authority. For a local filesystem source,
|
|
that authority is the node with access to the origin. For an adapter, it is the
|
|
node assigned to ingest that provider.
|
|
|
|
Subscribers can receive:
|
|
|
|
- a live query served by the authority;
|
|
- an on-disk source-store replica;
|
|
- a bounded metadata cache;
|
|
- dated frozen generations.
|
|
|
|
Generation updates are ordered by source watermarks. A subscriber applies them
|
|
in authority order. It does not merge two competing filesystem observations
|
|
with last-write-wins.
|
|
|
|
If an origin moves to another device, authority transfer is an explicit action
|
|
with a recorded handoff. Source identity survives the move.
|
|
|
|
## Assertions
|
|
|
|
Assertions represent knowledge that does not come from the origin, such as a
|
|
tag, note, permission, or review decision. Authorized devices can create
|
|
assertions independently while offline.
|
|
|
|
Every assertion carries provenance and logical time. Replicas merge changes by
|
|
the rule for that assertion type. A scalar preference may use last accepted
|
|
logical write. A tag attachment can preserve independent add and remove intent.
|
|
Security-sensitive policy can require explicit confirmation instead of silent
|
|
conflict resolution.
|
|
|
|
Filesystem modification time is never used to order assertion changes.
|
|
|
|
## Availability
|
|
|
|
Library sync keeps source presence separate from source availability.
|
|
|
|
| State | Meaning |
|
|
| --- | --- |
|
|
| Live | The origin and its serving node answer now |
|
|
| Degraded | Stored records remain available, but some queries or bytes cannot be served |
|
|
| Lost | The origin no longer answers; retained stores and snapshots are the evidence |
|
|
|
|
The interface can therefore show an offline drive, search its retained index,
|
|
and explain why opening a file is unavailable.
|
|
|
|
## Offline work
|
|
|
|
A device can browse attached stores and edit permitted assertions while peers
|
|
are offline. On reconnect:
|
|
|
|
1. Devices authenticate the existing pairing.
|
|
2. Each side exchanges library and assertion watermarks.
|
|
3. Missing changes are streamed and verified.
|
|
4. Source subscriptions resume from their own watermarks.
|
|
5. Availability is recalculated from current evidence.
|
|
|
|
No global leader is required for ordinary assertion sync. Source generation
|
|
still has an explicit authority.
|
|
|
|
## Remote queries
|
|
|
|
A paired device can serve source queries without copying the whole store first.
|
|
Queries are scoped by library membership, source grants, and subtree
|
|
permissions. Results identify their source and generation so a client can
|
|
detect stale cached data.
|
|
|
|
Search may combine local stores, cached catalogs, and live remote results. The
|
|
interface must distinguish a complete answer from a partial answer caused by
|
|
offline sources.
|
|
|
|
## Security boundary
|
|
|
|
Pairing grants a device identity, not unrestricted filesystem access.
|
|
Authorization is checked at operation time for:
|
|
|
|
- library membership;
|
|
- source and subtree scope;
|
|
- metadata visibility;
|
|
- byte reads;
|
|
- mutations;
|
|
- shares and mounts.
|
|
|
|
The same checks apply to graphical clients, CLI calls, extensions, and agents.
|
|
|
|
## Library sync is not file sync
|
|
|
|
Library sync can tell another device that a source contains a file. Remote
|
|
serving can open that file while the source is online. Neither action creates a
|
|
new durable copy.
|
|
|
|
[File Sync](/core/file-sync) is the explicit system that changes the placement
|
|
of bytes. Keeping it separate makes storage cost, deletion, conflicts, and
|
|
failure domains visible to the user.
|