fstree: support online depth reshaping - #4149
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #4149 +/- ##
==========================================
+ Coverage 30.32% 30.76% +0.44%
==========================================
Files 673 674 +1
Lines 40673 40986 +313
==========================================
+ Hits 12333 12611 +278
- Misses 28340 28375 +35 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| return fmt.Errorf("validate configuration: %w", err) | ||
| } | ||
| if blobstorLayoutChanged(oldCfg, newCfg) { | ||
| return errors.New("changing blobstor depth requires node restart") |
There was a problem hiding this comment.
Why can't it be done without it? Triggered via control service?
There was a problem hiding this comment.
Runtime layout changes require more than updating configuration: the node must locate the active FSTree instance, persist the new reshape state, atomically switch primary/fallback layouts, and synchronize this transition with concurrent PUT, DELETE, GET and iteration operations. It must also define what happens if configuration reload succeeds only for some shards or the reshape worker cannot start.
A restart performs this transition during storage initialization, before the node starts serving requests. Reshaping itself is still online, and the persistent descriptor state makes it safe to resume after later restarts. Given that, adding a separate runtime control path does not seem worth the additional complexity and risk.
| return err | ||
| } | ||
| t.reshapeMtx.Lock() | ||
| defer t.reshapeMtx.Unlock() |
There was a problem hiding this comment.
Not sure what you're trying to protect from.
There was a problem hiding this comment.
It prevents a delete/rename race that could resurrect a deleted object.
There was a problem hiding this comment.
rename is atomic. Proper sequencing can prevent this. Delete old path first, then delete new path. If old path is deleted before reshaper rename --- reshaper gets a rename error. If reshaper is first to rename then deleting old path fails, but deleting new path does.
| if err := util.MkdirAllX(filepath.Dir(newPath), t.Permissions); err != nil { | ||
| return false, fmt.Errorf("create destination directory for %q: %w", newPath, err) | ||
| } | ||
| if _, err := os.Lstat(newPath); err == nil { |
There was a problem hiding this comment.
Why? Things can change between lstat and rename. You're trying to avoid this with reshapeMtx, but this doesn't matter much anyway, objects with the same address are the same objects and rename can be done safely. Then reshapeIterMtx will drastically affect performance, this background process will almost always hold the lock and regular operations will suffer badly.
There was a problem hiding this comment.
I still think this lstat can be avoided.
b11b791 to
f7a4027
Compare
f7a4027 to
9ab6efa
Compare
| info, statErr := os.Stat(entry.path) | ||
| if statErr != nil { | ||
| return fmt.Errorf("stat file %q: %w", entry.path, statErr) | ||
| } | ||
| if err = sizeHandler(entry.addr, uint64(info.Size())); err != nil { | ||
| return err | ||
| } |
| d.Depth = d.Reshape.ToDepth | ||
| d.Reshape = nil | ||
| if err = writeDescriptor(descPath, d); err != nil { | ||
| return fmt.Errorf("write completed reshape descriptor: %w", err) |
f7e1ac1 to
314eee8
Compare
| return nil | ||
| } | ||
|
|
||
| func (t *FSTree) reshapeLastProcessedPath() (string, error) { |
There was a problem hiding this comment.
I'd expect it to be read once on FSTree init, parsing it again just adds some potential for errors.
|
|
||
| func (t *FSTree) updateReshapeProgress(lastProcessedPath string) error { | ||
| descPath := t.descriptorPath() | ||
| f, err := os.Open(descPath) |
There was a problem hiding this comment.
You can also keep the current version in mem, update last path there and flush it to disk, without reading the file.
| return err | ||
| } | ||
| t.reshapeMtx.Lock() | ||
| defer t.reshapeMtx.Unlock() |
There was a problem hiding this comment.
rename is atomic. Proper sequencing can prevent this. Delete old path first, then delete new path. If old path is deleted before reshaper rename --- reshaper gets a rename error. If reshaper is first to rename then deleting old path fails, but deleting new path does.
| func (t *FSTree) getPath(addr oid.Address) (string, error) { | ||
| p := t.treePath(addr) | ||
| t.reshapeMtx.RLock() | ||
| defer t.reshapeMtx.RUnlock() |
There was a problem hiding this comment.
Same with all other operations, old first, new afterwards --- no locking needed.
| } | ||
|
|
||
| t.reshapeMtx.Lock() | ||
| defer t.reshapeMtx.Unlock() |
There was a problem hiding this comment.
Puts don't care at all, they always use new path.
| return err | ||
| } | ||
| *dirty = false | ||
| if err := t.updateReshapeProgress(relativePath); err != nil { |
There was a problem hiding this comment.
You can write this file and have a single syncfs call for it and other meta (avoid syncReshape).
| if err := util.MkdirAllX(filepath.Dir(newPath), t.Permissions); err != nil { | ||
| return false, fmt.Errorf("create destination directory for %q: %w", newPath, err) | ||
| } | ||
| if _, err := os.Lstat(newPath); err == nil { |
There was a problem hiding this comment.
I still think this lstat can be avoided.
| } | ||
| } | ||
| } | ||
|
|
c153a12 to
ed2d035
Compare
Add secondary-depth fallback and background file relocation. Require restart for FSTree layout changes. Closes #3774. Signed-off-by: Andrey Butusov <andrey@nspcc.io>
Closes #3774.