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 distinct Serve implementations across the codebase.
  • Example: lib/model/folder.go:149func (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 suture library brings Erlang-OTP-style supervision: transient errors restart the service, svcutil.FatalErr propagates 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 calling supervisor.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: processNeededcopierRoutine (multiple) → pullerRoutinefinisherRoutinedbUpdaterRoutine. 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 separate sync.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 in lib/scanner/blockqueue.go:87.
  • Example: lib/model/folder_sendrecv.go:265-270 — the number of copier goroutines is configured via FolderConfiguration.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.Context across the project. All Serve methods accept ctx context.Context from suture. context.WithCancel and context.WithTimeout are used throughout tests and in production code.
  • Example: lib/model/model.go:283case <-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 use context.WithTimeout to prevent hangs.
  • Assessment: Idiomatic and pervasive. Suture passes a live context to each Serve method and cancels it when the service should stop — goroutines that select on ctx.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-device connRequestLimiters, and folderIOLimiter (limits per-folder concurrent I/O). lib/semaphore.MultiSemaphore allows atomic acquisition from multiple limiters.
  • Example: lib/model/model.go:2016-2095newLimitedRequestResponse acquires from up to three semaphores; the RAII-style requestResponse.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 Serve method. Individual services select on ctx.Done() to exit cleanly. The App.Stop() method cancels the root context.
  • Example: lib/svcutil/svcutil.goFatalErr and noRestartErr types 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.Once appears 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 shutdown
    • svcutil.noRestartErr — marks a clean exit that should not trigger a restart
    • model.FileError (lib/model/folder_sendrecv.go:2178) — wraps a file path with an error
    • fs.CaseConflictError — case-sensitive vs case-insensitive filesystem conflict
    • fs.WatchEventOutsideRootError — filesystem watcher event outside the monitored root
    • osutil.TraversesSymlinkError, osutil.NotADirectoryError — fs traversal errors
    • ignore.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). No github.com/pkg/errors dependency. errors.Is and errors.As appear 114 times — the project makes full use of Go 1.13+ error unwrapping.
  • Examples:
    • lib/model/folder_recvenc.go:73fmt.Errorf("deleting unexpected item: %w", err) — wraps OS errors with context
    • lib/model/folder_sendrecv_test.go:994errors.As(err, &caseErr) — tests use type assertion on wrapped errors

Configuration pattern#

  • Approach: Config struct (lib/config.Configuration) with XML/JSON tags, loaded from config.xml. For component-level configuration, functional options appear in specific packages.
  • Functional options example: lib/fs/mtimefs.go:29-35type MtimeFSOption func(*mtimeFS) with WithCaseInsensitivity(v bool) MtimeFSOption; lib/ignore/ignore.go:136-148type Option func(*Matcher) with WithCache and WithChangeDetector; internal/db/sqlite/db_open.go:40-46WithDeleteRetention(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 registered Committer subscribers. 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.go startup() is the single wiring site — it constructs all components in dependency order, passing interfaces as constructor arguments. All New* functions take explicit interface parameters (e.g., model.NewModel(cfg config.Wrapper, id protocol.DeviceID, db db.DB, ...).
  • Circular dependency resolution: A lateAddressLister wrapper breaks the connections ↔ discover cycle: it is constructed as a zero-value placeholder, both services are constructed with it, and then it is back-filled with the real AddressLister once connections.Service exists. 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.Service is 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.Service for 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/fs platform-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.