Skip to main content
go-swarm-icons
On this page

Thread Safety

See which synchronization primitive guards each component for safe concurrent access.

Every component in go-swarm-icons is safe for concurrent use. This page documents the specific synchronization mechanism protecting each type.

Concurrency guarantee

All public types in the library are safe for concurrent use from multiple goroutines. No external synchronization is needed. This guarantee is achieved through a combination of sync.RWMutex, sync.Once, and structural immutability.

Concurrency model

Component Mechanism Protected state
IconManager sync.RWMutex providers map, aliases map, default prefix, fallback config, renderer reference
DirectoryProvider iconCache (sync.RWMutex) resolved icon cache
JsonCollectionProvider sync.Once + iconCache (sync.RWMutex) lazy JSON parse + resolved icon cache
IconifyProvider iconCache (sync.RWMutex) HTTP response cache
SpriteCollector sync.RWMutex symbols map
Icon Immutability No mutable state

The iconCache type

Three of the four providers (DirectoryProvider, JsonCollectionProvider, IconifyProvider) share the same caching primitive: iconCache (defined in cache.go). It wraps a sync.RWMutex and a map[string]*Icon.

The cache uses a double-checked locking pattern in GetOrLoad:

1. RLock
2. Check map for name
3. If found: RUnlock, return cached icon
4. RUnlock
5. Call the loader function (no lock held)
6. If loader returns (nil, false): return not found
7. Lock (write)
8. Check map again (another goroutine may have stored it)
9. If found: Unlock, return the other goroutine's result
10. Store the new icon
11. Unlock, return the new icon

This pattern has two important properties:

The read path is fast: Under normal load (cache populated), every Get call acquires only a read lock, checks the map, and returns. Read locks do not contend with each other, so concurrent reads scale linearly with goroutine count.

The loader runs without holding a lock: Step 5 calls the provider-specific loading function (disk I/O, JSON resolution, or HTTP request) without any lock held. This prevents slow I/O from blocking other goroutines that are reading cached icons. The trade-off is that two goroutines racing on the same uncached name both run the loader, and the first to acquire the write lock wins. The second goroutine's result is discarded. This is acceptable because loader results are deterministic and the race is rare.

IconManager locking

The manager's Get method minimizes lock hold time by copying all configuration values into local variables under a single read lock, then releasing the lock before proceeding with resolution:

1. RLock
2. Copy: defaultPrefix, providers, fallbackIcon, prefixFallbacks,
ignoreNotFound, renderer
3. RUnlock
4. Proceed with resolution using local copies (no lock held)

This means that configuration changes (via SetDefaultPrefix, SetAlias, Register, etc.) made by another goroutine during resolution do not affect the in-progress Get call. The call sees a consistent snapshot of the configuration as of step 2.

Mutation methods (Register, SetAlias, SetDefaultPrefix, SetFallbackIcon, SetFallbackIconForPrefix, SetIgnoreNotFound, SetRenderer) acquire a write lock for the duration of the map or field write. These operations are fast (map insertion or field assignment) and do not perform I/O.

JsonCollectionProvider lazy parse

The JsonCollectionProvider uses sync.Once to ensure the JSON collection is parsed exactly once, regardless of how many goroutines call Get, Has, or All concurrently. The first goroutine to call load() runs json.Unmarshal; all other goroutines block on the Once until parsing completes, then proceed with the parsed data.

After the Once completes, the parsed data (*jsonCollection) is read-only. Alias resolution and icon construction happen per-call and store results in the iconCache, which has its own RWMutex.

Icon immutability

The Icon type has no exported fields and no mutating methods. Every fluent method (Size, Class, Fill, Rotate, etc.) calls clone(), which creates a new *Icon with a deep-copied attribute map. The original icon is never modified.

This means:

  • Provider caches store *Icon values that are never mutated by consumer code.
  • Multiple goroutines can call Get for the same icon name, apply different transformations, and produce independent results without coordination.
  • No defensive copying is needed when passing *Icon values between functions or goroutines.

The Attributes() method returns a deep copy of the attribute map, so callers cannot mutate the icon's state through the returned map.

SpriteCollector locking

Register acquires a write lock to insert into the symbols map. SpriteSheet acquires a read lock to iterate the map. Reset acquires a write lock to replace the map. All three operations are safe to call from multiple goroutines.

See also

  • Performance & Caching: practical caching guidance and provider selection for throughput
  • Architecture: how these synchronization mechanisms compose in the resolution flow
Edit this page

Last updated: