Skip to content

Feature/tiered storage - #1049

Open
foodprocessor wants to merge 104 commits into
mainfrom
feature/tiered_storage
Open

foodprocessor wants to merge 104 commits into
mainfrom
feature/tiered_storage

Conversation

@foodprocessor

Copy link
Copy Markdown
Contributor

Add tiered storage component.

jfantinhardesty and others added 30 commits May 27, 2026 10:57
… with no file attatched, also added some config path
@foodprocessor
foodprocessor marked this pull request as draft September 11, 2026 06:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several rename, pagination, accounting, and persistence defects can cause hangs, inconsistent listings, or data loss.

16 open findings
What changed in this PR

Adds a local-primary tiered storage component with cloud overflow, LRU eviction, restart recovery, and cross-platform support.

Changes:

  • Implements tiered file operations, persistence, capacity tracking, and eviction.
  • Adds configuration, documentation, registration, and pipeline validation.
  • Adds unit tests and an S3 mount smoke test.
File Description
.github/​workflows/​unit-test.yml Adds tiered-storage smoke testing.
README.md Documents tiered-storage usage.
cmd/​imports.go Registers the new component.
cmd/​mount_linux_test.go Verifies cleanup preserves tiered data.
common/​lock_map.go Adds atomic handle counts and try-locking.
common/​util.go Extends pipeline validation.
common/​util_test.go Tests tiered pipeline combinations.
component/​tiered_storage/​cache_size.go Tracks local capacity usage.
component/​tiered_storage/​lru_policy.go Implements parallel LRU eviction.
component/​tiered_storage/​lru_policy_test.go Tests eviction behavior.
component/​tiered_storage/​persistence.go Persists and recovers local state.
component/​tiered_storage/​tiered_storage.go Implements the core component.
component/​tiered_storage/​tiered_storage_linux.go Adds Linux filesystem support.
component/​tiered_storage/​tiered_storage_linux_test.go Tests Linux ownership persistence.
component/​tiered_storage/​tiered_storage_test.go Tests component operations and recovery.
component/​tiered_storage/​tiered_storage_windows.go Adds Windows filesystem support.
sample_configs/​sampleTieredStorageConfigS3.yaml Provides an S3 example.
setup/​advancedConfig.yaml Documents advanced configuration.
setup/​baseConfig.yaml Adds baseline configuration options.
testdata/​config/​s3_key_tiered_storage.yaml Adds smoke-test configuration.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +41 to +42
tieredStorageSnapshotPath = ".tieredStorageSnapshot.gob"
tieredStorageSnapshotVersion = 1
Comment on lines +747 to +750
if rmErr := os.Remove(dst); rmErr != nil && !errors.Is(rmErr, os.ErrNotExist) {
return err
}
return os.Rename(src, dst)
Comment on lines +814 to +817
var node *FileNode
if val, ok := c.fileMap.Load(options.Handle.Path); ok {
node = val.(*FileNode)
}
Comment on lines +836 to +850
if err == nil {
c.setHandleDirty(options.Handle)
if node != nil {
node.isDirty.Store(true)
if info, statErr := f.Stat(); statErr == nil {
c.cacheSize.Add(info.Size() - node.size.Swap(info.Size()))
}
}
} else {
log.Err(
"TieredStorage::WriteFile : failed to write %s [%s]",
options.Handle.Path,
err.Error(),
)
}
}

func (c *TieredStorage) RenameFile(options internal.RenameFileOptions) error {
log.Trace("TieredStorage::RenameFile : src=%s, dst=%s", options.Src, options.Dst)
Comment on lines +361 to +364
if token == "" {
for name, attr := range localAttrs {
if _, found := listed[name]; !found {
attrs = append(attrs, attr)
Comment on lines +1046 to +1048
if err := os.Rename(srcPath, dstPath); err != nil {
return err
}
Comment on lines +1317 to +1322
available := max(int64(c.maxCacheSize)-c.cacheSize.Used(), 0)
stat := &common.Statfs_t{
Blocks: uint64(c.maxCacheSize) / blockSize,
Bavail: uint64(available) / blockSize,
Bfree: physicalFree / blockSize,
Bsize: blockSize,
Comment thread README.md
### Expanding Existing Local Storage Using Tiered Storage

When using `tiered_storage` to add cloud capacity to an existing local storage location, please ensure all access is done through the Cloudfuse mount path / virtual drive. The `tiered_storage` `path` and all its existing contents will become *internal* local storage. Accessing the `tiered_storage` `path` directly may have unpredictable results.
For example, if you want expand the capacity of your existing local drive, `D:`, you could change that local drive's letter to `E:` first, then set `tiered_storage` `path` to `E:` and mount cloudfuse to `D:`. Then any existing applications or workflows that normally use `D:` would continue as normal, now pointed at the cloudfuse mount location.
Comment thread setup/advancedConfig.yaml
Comment on lines +138 to +139
high-threshold: <% local storage consumed which triggers eviction. Default - 80>
low-threshold: <% local storage consumed which eviction targets. Must be less than high-threshold. Default - 60>
if options.Handle == nil {
handle, err := c.OpenFile(internal.OpenFileOptions{
Name: options.Name,
Flags: os.O_RDWR,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no open handle and new size is 0, we should probably just open with O_TRUNC, so we don't end up downloading the whole file. I think some applications do that.

return nil
}

func (c *TieredStorage) RenameFile(options internal.RenameFileOptions) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think rename has some edge cases to think through. If the src is in the cloud only, and dst is local only, then doing that rename, I think will end up uploading the dst once eviction happens and not the src. So I think it should check if dst exists, and delete the local copy and remove it from the queue.

c.policy.Dequeue(options.Name)
c.fileMap.Delete(options.Name)

return c.purgeLocal(options.Name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other parts of the code do purgeLocal first, and then the deletion happens, in case it fails (which maybe could happen on Windows if the file is open). So I think that pattern should follow here.

return c.NextComponent().CloseDir(options)
}

func (c *TieredStorage) RenameDir(options internal.RenameDirOptions) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we are doing local first then cloud. In case there is an issue with renaming a file in the cloud, probably safer to do cloud first then local.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we probably need some locks when doing a directory rename. I could imagine some race conditions that could probably come up here.

type persistedFileState struct {
Size int64
Mtime int64
CloudBacked bool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an edge case concern with cloudBacked. It currently does two things (Is there an object in the cloud with this name, and is this file in the cloud and the local copy is a temp copy). Most times these are the same, but not always. Consider a local file A and a cloud file B. Then we mv A B. The answer to 1 is yes, but two is no. But cloudBaked would be copied from A, which would say no. Then the old B is still in the cloud and does not get removed until later.

capacityPollInterval = time.Second
reconcileCapacityInterval = 5 * time.Minute
partialDownloadSuffix = ".cloudfuse-partial"
// Cache holds user data and is only accessed by this process (CWE-732).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for storing these in the mount path, and not in a separate folder?

Comment thread common/lock_map.go
l.mtx.Unlock()
}

func (l *LockMapItem) TryLock() bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an anti pattern. Go says that correct uses of TryLock are rare, and I'm not sure it is correctly used here. It probably points to a bigger locking problem.

}

// EvictNow synchronously makes room for growBytes.
func (q *lruQueue) EvictNow(growBytes int64) bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems expensive to do eviction synchronously. Any reason to not follow file cache and do eviction in the background?

This branch was successfully deployed

1 active deployment
testing — a9be77ba Deployed Oct 1, 2026 by foodprocessor via Cleanup Test Accounts (1.27, linux) #3809
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants