Documentation
¶
Overview ¶
Package storage provides interfaces and implementations for persisting and retrieving pod data, as local file-based storage backed by an in-memory cache.
Ownership contract: the cache owns its values. Every resource that crosses the interface — in or out — is deep-copied, so a value returned by GetResource or GetAll is the caller's private copy and a value handed to AddResource is not aliased by the cache afterwards. Callers are therefore free to mutate what they get back; to make a change stick they write it back with AddResource. See the CachedLocalStorage doc for why (a shared pointer here was a real data race between the CNI server's termination watch and its liveness loop).
Index ¶
- type CachedLocalStorage
- func (f *CachedLocalStorage[T]) AddResource(_ context.Context, key types.ContainerID, resource T) error
- func (f *CachedLocalStorage[T]) GetAll(_ context.Context) ([]T, error)
- func (f *CachedLocalStorage[T]) GetResource(_ context.Context, key types.ContainerID) (T, error)
- func (f *CachedLocalStorage[T]) Initialize(ctx context.Context) error
- func (f *CachedLocalStorage[T]) Reload(_ context.Context) (Delta[T], error)
- func (f *CachedLocalStorage[T]) RemoveResource(_ context.Context, key types.ContainerID) error
- func (f *CachedLocalStorage[T]) WaitUntilReady(ctx context.Context) error
- type Delta
- type MockStorage
- func (m *MockStorage[T]) AddResource(ctx context.Context, key types.ContainerID, resource T) error
- func (m *MockStorage[T]) GetAll(ctx context.Context) ([]T, error)
- func (m *MockStorage[T]) GetResource(ctx context.Context, key types.ContainerID) (T, error)
- func (m *MockStorage[T]) Initialize(_ context.Context) error
- func (m *MockStorage[T]) Reload(ctx context.Context) (Delta[T], error)
- func (m *MockStorage[T]) RemoveResource(ctx context.Context, key types.ContainerID) error
- func (m *MockStorage[T]) WaitUntilReady(_ context.Context) error
- type Reloader
- type Storage
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CachedLocalStorage ¶
CachedLocalStorage is a storage implementation that persists resources to the local filesystem and maintains an in-memory cache for fast access. Each resource is stored as a JSON file.
It is safe for concurrent access, and the guarantee covers the VALUES as well as the map: the RWMutex alone only ever protected the map, so handing out the cache's own pointers made every consumer of GetAll a potential writer of somebody else's read. That was a real race — the CNI server's termination watch set CNIPod.Terminating in place (under its own lifecycleMu) while the liveness ticker read the same pointer holding no lock at all, and AddResource's protojson.Marshal read it a third time. So every value crossing this boundary is deep-copied: AddResource caches a clone of what it is given, GetResource and GetAll return clones of what they hold. A caller owns what it gets back and may mutate it freely; to make the change stick it writes the value back with AddResource (copy-on-write, which is what the two CNI call sites do).
The copies are not free but they are cheap where it matters: the cached resources are node-local pod records (tens per node), the readers are a 5s liveness tick and a 60s sweep, and AddResource already marshals to JSON and fsyncs a file — a clone is noise next to that.
func NewCachedLocalStorage ¶
func NewCachedLocalStorage[T proto.Message](basePath string, newFunc func() T) *CachedLocalStorage[T]
NewCachedLocalStorage creates a new CachedLocalStorage instance. Resources are stored as individual JSON files in the specified basePath and cached in memory. The newFunc parameter is a factory function that creates new instances of type T for unmarshaling.
func (*CachedLocalStorage[T]) AddResource ¶
func (f *CachedLocalStorage[T]) AddResource(_ context.Context, key types.ContainerID, resource T) error
AddResource stores a resource atomically to disk and updates the in-memory cache. The resource is serialized as JSON and written atomically to ensure consistency.
func (*CachedLocalStorage[T]) GetAll ¶
func (f *CachedLocalStorage[T]) GetAll(_ context.Context) ([]T, error)
GetAll returns all cached resources as a slice. Resources must be initialized before calling this method. Each element is a deep copy the caller owns (see CachedLocalStorage).
func (*CachedLocalStorage[T]) GetResource ¶
func (f *CachedLocalStorage[T]) GetResource(_ context.Context, key types.ContainerID) (T, error)
GetResource retrieves a resource by key, checking the in-memory cache first. If not in cache, it loads the resource from disk and caches it for future access. The returned message is a deep copy the caller owns (see CachedLocalStorage).
func (*CachedLocalStorage[T]) Initialize ¶
func (f *CachedLocalStorage[T]) Initialize(ctx context.Context) error
Initialize loads all resources from disk into the in-memory cache. It is safe to call multiple times; only the first call performs initialization. Subsequent calls return the result of the first initialization.
func (*CachedLocalStorage[T]) Reload ¶
func (f *CachedLocalStorage[T]) Reload(_ context.Context) (Delta[T], error)
Reload re-reads the directory and replaces the in-memory view with it, returning the difference (see Reloader). The directory is read under the write lock, so this process's own AddResource/RemoveResource serialize around the reload rather than being lost by its swap. A file that cannot be read or decoded fails the whole reload and leaves the view untouched: a half-applied diff would be worse than the previous, internally consistent view.
func (*CachedLocalStorage[T]) RemoveResource ¶
func (f *CachedLocalStorage[T]) RemoveResource(_ context.Context, key types.ContainerID) error
RemoveResource deletes a resource from disk and the in-memory cache. It does not return an error if the resource file does not exist.
func (*CachedLocalStorage[T]) WaitUntilReady ¶
func (f *CachedLocalStorage[T]) WaitUntilReady(ctx context.Context) error
WaitUntilReady blocks until the cache is populated from disk or the context is canceled. It should be called before using other methods to ensure all resources are loaded.
type Delta ¶
type Delta[T proto.Message] struct { // Added holds resources present on disk that the previous view did not have. Added []T // Updated holds resources present in both views whose content differs; the // value is the one now on disk. Updated []T // Removed holds resources the previous view had that are no longer on disk; // the value is the one the previous view held (what the caller built from). Removed []T }
Delta is the difference between two views of a storage directory, by key.
type MockStorage ¶
type MockStorage[T proto.Message] struct { // GetAllFunc is called by GetAll. If nil, GetAll iterates the resources map. GetAllFunc func(ctx context.Context) ([]T, error) // AddResourceFunc is called by AddResource. If nil, the resource is stored in the map. AddResourceFunc func(ctx context.Context, key types.ContainerID, resource T) error // GetResourceFunc is called by GetResource. If nil, the resource is looked up in the map. // Return os.ErrNotExist (or any error satisfying os.IsNotExist) to simulate a missing file. GetResourceFunc func(ctx context.Context, key types.ContainerID) (T, error) // RemoveResourceFunc is called by RemoveResource. If nil, the resource is deleted from the map. RemoveResourceFunc func(ctx context.Context, key types.ContainerID) error // ReloadFunc is called by Reload. If nil, Reload reports no difference. ReloadFunc func(ctx context.Context) (Delta[T], error) // contains filtered or unexported fields }
MockStorage is an in-memory Storage implementation intended for use in tests outside the storage package. Because Storage contains the unexported loadAll method, types in other packages cannot implement the interface directly; MockStorage bridges that gap by living in package storage while exposing configurable behaviour via exported function fields.
Like LocalStorage — the implementation it stands in for — MockStorage is safe for concurrent use. Production callers (the CNI server's liveness loop, ghost sweep and drain timers) read and write storage from several goroutines, so a double without that guarantee turns any test of those paths into a data race on the backing map rather than a test of the code under test. The configurable function fields are NOT guarded: set them before the storage is shared.
It also honours the package's ownership contract: the map path deep-copies every value in and out, so a test double can never hand two goroutines the same proto pointer when the real storage would not. A GetAllFunc/ GetResourceFunc override is the test's own business and is returned verbatim.
func NewMockStorage ¶
func NewMockStorage[T proto.Message]() *MockStorage[T]
NewMockStorage returns a MockStorage with an empty resource map.
func NewMockStorageWithGetAll ¶
func NewMockStorageWithGetAll[T proto.Message](fn func(ctx context.Context) ([]T, error)) *MockStorage[T]
NewMockStorageWithGetAll returns a MockStorage whose GetAll method is backed by the provided function. Use this when test cases need precise control over the data returned by GetAll, including error injection.
func (*MockStorage[T]) AddResource ¶
func (m *MockStorage[T]) AddResource(ctx context.Context, key types.ContainerID, resource T) error
func (*MockStorage[T]) GetResource ¶
func (m *MockStorage[T]) GetResource(ctx context.Context, key types.ContainerID) (T, error)
func (*MockStorage[T]) Initialize ¶
func (m *MockStorage[T]) Initialize(_ context.Context) error
func (*MockStorage[T]) Reload ¶
func (m *MockStorage[T]) Reload(ctx context.Context) (Delta[T], error)
Reload satisfies Reloader. The mock's map is its own disk, so there is never another writer's change to find unless the test scripts one with ReloadFunc.
func (*MockStorage[T]) RemoveResource ¶
func (m *MockStorage[T]) RemoveResource(ctx context.Context, key types.ContainerID) error
func (*MockStorage[T]) WaitUntilReady ¶
func (m *MockStorage[T]) WaitUntilReady(_ context.Context) error
type Reloader ¶
type Reloader[T proto.Message] interface { // Reload re-reads every resource from disk, replaces the in-memory view with // it, and returns how it differs from the view it replaced. Every value in // the returned Delta is a deep copy the caller owns. Reload(ctx context.Context) (Delta[T], error) }
Reloader is a Storage whose in-memory view can be re-synchronised with disk, reporting what another writer changed since this process last looked.
It exists for the node agent's surge handoff (proposal 041): a standby agent loads storage at its start, but until it takes the node over the OLD agent is the one serving CNI ADD/DEL and writing these files. On takeover the standby reloads and applies only the difference — a full listener rebuild costs 0.25-0.98 s on the reference cluster, inside the window the node's proxy has no ADS stream.
type Storage ¶
type Storage[T proto.Message] interface { // Initialize prepares the storage for use, including creating directories // and loading initial data if necessary. Initialize(ctx context.Context) error // WaitUntilReady waits until the storage is ready to serve requests. // This may involve waiting for file watching mechanisms to be initialized. WaitUntilReady(ctx context.Context) error // AddResource stores a new resource with the given key. The implementation // keeps a deep copy, so the caller may keep mutating its own value without // affecting what is stored. AddResource(ctx context.Context, key types.ContainerID, resource T) error // RemoveResource deletes a resource by key. RemoveResource(ctx context.Context, key types.ContainerID) error // GetResource retrieves a single resource by key. The returned message is a // deep copy the caller owns: mutating it does not affect the stored resource // (write it back with AddResource to persist a change). GetResource(ctx context.Context, key types.ContainerID) (T, error) // GetAll returns all stored resources from the in-memory cache. Each element // is a deep copy the caller owns — see GetResource. GetAll(_ context.Context) ([]T, error) // contains filtered or unexported methods }
Storage defines an interface for storing and retrieving protobuf messages. Implementations manage the lifecycle of resources and maintain consistency between persistent storage and in-memory caches.