Syncthing — Patterns#
Concurrency patterns#
Supervisor tree (suture)#
- Usage: Every major service — model, connections, discovery, events, config, API, NAT — implements
suture.Service(Serve(ctx context.Context) error). All are registered under a root*suture.Supervisor. At least 15 distinctServeimplementations across the codebase. - Example:
lib/model/folder.go:149—func (f *folder) Serve(ctx context.Context) error— each per-folder runner is a supervised service;lib/events/events.go:297— the event logger itself is supervised. - Assessment: Unusual in Go, well-executed here. The
suturelibrary brings Erlang-OTP-style supervision: transient errors restart the service,svcutil.FatalErrpropagates upward and shuts down the process. The pattern provides fault isolation at zero additional concurrency boilerplate — adding a new subsystem means implementing one method and callingsupervisor.Add(service).
Channel-based pipeline (fan-out/fan-in)#
- Usage: The
sendReceiveFolder.pullerIteration()method orchestrates the entire file-sync pipeline as a channel-connected sequence of goroutines:processNeeded→copierRoutine(multiple) →pullerRoutine→finisherRoutine→dbUpdaterRoutine. Channels are typed (chan copyBlocksState,chan pullBlockState,chan *sharedPullerState,chan dbUpdateJob) and directional at usage sites. - Example:
lib/model/folder_sendrecv.go:247-307— four separatesync.WaitGroups (copyWg,pullWg,doneWg,updateWg) gate each stage; channels are closed in order to signal downstream completion. - Assessment: Textbook fan-out/fan-in Go pipeline. Each stage is clearly separated. Shutdown is orderly: close copyChan → wait copiers → close pullChan → wait puller → close finisherChan → wait finisher → close dbUpdateChan → wait updater. No goroutine leaks possible, every stage has a well-defined termination condition. High quality.
Worker pool#
- Usage:
for range f.Copiers { copyWg.Go(func() { f.copierRoutine(...) }) }— multiple copier goroutines are spawned from a configurable count. Similar pattern exists inlib/scanner/blockqueue.go:87. - Example:
lib/model/folder_sendrecv.go:265-270— the number of copier goroutines is configured viaFolderConfiguration.Copiers. - Assessment: Simple, idiomatic. Pool size is runtime-configurable via config. Workers self-terminate when their input channel closes, so the pool needs no explicit shutdown mechanism beyond channel close.
Context cancellation#
- Usage: 359 uses of
context.Contextacross the project. AllServemethods acceptctx context.Contextfrom suture.context.WithCancelandcontext.WithTimeoutare used throughout tests and in production code. - Example:
lib/model/model.go:283—case <-ctx.Done(): l.Debugln(m, "context closed, stopping", ctx.Err())— the model’s main loop exits on context cancellation;lib/model/folder_sendrecv_test.go:669— tests usecontext.WithTimeoutto prevent hangs. - Assessment: Idiomatic and pervasive. Suture passes a live context to each
Servemethod and cancels it when the service should stop — goroutines that select onctx.Done()integrate cleanly with supervision.
Semaphore-based rate limiting#
- Usage: Three semaphores regulate I/O and concurrency:
globalRequestLimiter(limits total pending incoming request bytes), per-deviceconnRequestLimiters, andfolderIOLimiter(limits per-folder concurrent I/O).lib/semaphore.MultiSemaphoreallows atomic acquisition from multiple limiters. - Example:
lib/model/model.go:2016-2095—newLimitedRequestResponseacquires from up to three semaphores; the RAII-stylerequestResponse.Close()releases them automatically. - Assessment: Custom semaphore implementation rather than
golang.org/x/sync/semaphore. The multi-semaphore pattern is elegant and prevents double-counting. Rate limiting is a first-class concern, not an afterthought.
Graceful shutdown#
- Usage: Suture handles shutdown by cancelling the context passed to each
Servemethod. Individual services select onctx.Done()to exit cleanly. TheApp.Stop()method cancels the root context. - Example:
lib/svcutil/svcutil.go—FatalErrandnoRestartErrtypes control whether suture restarts a failed service or propagates the error upward. The pipeline uses ordered channel close (see above) to drain in-flight work before exiting. - Assessment: Shutdown correctness is well thought-out. The suture integration means services don’t need to manage their own restart logic; they just return an error, and the supervisor decides what to do based on the error type.
Sync primitives#
- Usage: 320 total uses of sync primitives (
sync.Mutex,sync.RWMutex,sync.Once,sync.WaitGroup,sync.Map,atomic.*).sync.Onceappears 16 times;atomic.*33 times. - Assessment: Heavy but appropriate. The model struct has many
sync.RWMutex-guarded maps (connections, folder runners, request limiters). Atomics are used for statistics counters and flags.
Error handling#
- Style: Mixed: sentinel errors for expected states, custom struct types for domain errors,
fmt.Errorf("%w", ...)for wrapping. - Error types defined:
svcutil.FatalErr— marks an error as non-retriable by suture; causes supervisor shutdownsvcutil.noRestartErr— marks a clean exit that should not trigger a restartmodel.FileError(lib/model/folder_sendrecv.go:2178) — wraps a file path with an errorfs.CaseConflictError— case-sensitive vs case-insensitive filesystem conflictfs.WatchEventOutsideRootError— filesystem watcher event outside the monitored rootosutil.TraversesSymlinkError,osutil.NotADirectoryError— fs traversal errorsignore.ParseError— structured parse error for .stignore files- Sentinel errors in model:
errDeviceUnknown,errDevicePaused,ErrFolderPaused,ErrFolderNotRunning,ErrFolderMissing
- Wrapping approach:
fmt.Errorf("%w", err)exclusively (205 occurrences). Nogithub.com/pkg/errorsdependency.errors.Isanderrors.Asappear 114 times — the project makes full use of Go 1.13+ error unwrapping. - Examples:
lib/model/folder_recvenc.go:73—fmt.Errorf("deleting unexpected item: %w", err)— wraps OS errors with contextlib/model/folder_sendrecv_test.go:994—errors.As(err, &caseErr)— tests use type assertion on wrapped errors
Configuration pattern#
- Approach: Config struct (
lib/config.Configuration) with XML/JSON tags, loaded fromconfig.xml. For component-level configuration, functional options appear in specific packages. - Functional options example:
lib/fs/mtimefs.go:29-35—type MtimeFSOption func(*mtimeFS)withWithCaseInsensitivity(v bool) MtimeFSOption;lib/ignore/ignore.go:136-148—type Option func(*Matcher)withWithCacheandWithChangeDetector;internal/db/sqlite/db_open.go:40-46—WithDeleteRetention(d time.Duration) Option. - Not universal: The main application configuration uses a struct + XML, not functional options. Functional options appear only for lower-level packages where callers rarely need to specify all parameters.
- Live reload:
config.Wrapper.Modify(fn)transactionally applies changes and notifies all registeredCommittersubscribers. The pattern avoids the need to restart the daemon for most config changes.
Dependency injection#
- Approach: Fully manual constructor injection. No framework.
- Evidence:
lib/syncthing/syncthing.gostartup()is the single wiring site — it constructs all components in dependency order, passing interfaces as constructor arguments. AllNew*functions take explicit interface parameters (e.g.,model.NewModel(cfg config.Wrapper, id protocol.DeviceID, db db.DB, ...). - Circular dependency resolution: A
lateAddressListerwrapper breaks theconnections ↔ discovercycle: it is constructed as a zero-value placeholder, both services are constructed with it, and then it is back-filled with the realAddressListeronceconnections.Serviceexists. This is the only place where a “lazy pointer” DI trick is used.
Other notable patterns#
Generic serviceMap#
The serviceMap[K comparable, S suture.Service] type (lib/model/service_map.go) is the clearest use of generics in the project. It implements a supervised registry: key-value map where each value is a suture service, with Add, Get, Remove, RemoveAndWait, Each, and its own Serve method. Used as serviceMap[protocol.DeviceID, *indexHandlerRegistry] for index handlers and as serviceMap[string, service] for per-folder runners.
- Assessment: Generics applied to a real abstraction, not a toy example. The type constraint
S suture.Serviceis meaningful — it guarantees the values can be supervised. Avoids map + loop + type-assert boilerplate that would appear in a pre-generics version.
Interface embedding for composing contracts#
lib/discover defines FinderService as an interface that embeds both Finder and suture.Service, expressing “a discovery mechanism that can also be supervised.” The same pattern appears in events.Logger (embeds suture.Service) and db.DBService (extends the DB interface with a supervised lifecycle).
- Assessment: Clean use of interface composition to combine domain contract with lifecycle contract without inheritance.
Observer / event bus#
lib/events.Logger provides a typed, async, in-process event bus. Components emit events.Event{Type, Time, Data} via Logger.Log(eventType, data). Consumers call Logger.Subscribe(eventTypeMask) to receive a Subscription channel. The REST API long-polls subscriptions over HTTP. Event types are bit flags, allowing subscriptions to multiple types with a single mask.
- Assessment: Well-designed event bus. Type bitmask subscription is efficient — a subscriber interested in N event types holds one channel, not N. The bus implements
suture.Servicefor clean shutdown. Decouples model, connections, and API without direct dependencies.
Type switches#
33 type switches throughout the codebase. Used primarily in the protocol layer (dispatching on message type), the filesystem layer (dispatching on OS-specific error types), and config migration code.
- Example: Protocol message dispatch,
lib/fsplatform-specific error wrapping. - Assessment: Appropriate use. Not overused; type switches appear where a closed set of types genuinely requires different dispatch logic.
configMuxBuilder — limited builder pattern#
lib/api/confighandler.go uses a configMuxBuilder struct with a fluent register* method family to register REST config endpoints. The builder encapsulates the mux, model reference, and response helpers, reducing per-handler boilerplate.
- Assessment: Builder as a scoped helper, not a public API. Effective for eliminating repetition across 10+ similar config endpoints.
Table-driven tests#
Prevalent. Patterns found: testCases := []struct{...}, tests := []struct{...}, and map-keyed test cases. Examples: lib/model/model_test.go:3768, lib/model/blockpullreorderer_test.go:24.
- Assessment: Standard Go style, well-applied. Tests use named struct fields for readability. The project also uses
t.Context()(Go 1.21+) in tests rather than background context, which integrates with test timeout handling.