X review feedback on newapi
- add nodefs package overview; in particular describe what inode is so that there is no confusion about its meaning. - Draftly implement Lookup / Forget. They work not under global rawBridge.mu and still there should be no race of Lookup / Forget due to careful locking of inode -> (inode, ichild) + retrying. Add description of Forget semantic and what happens when we receive forget for a directory for which children are not forgotten yet. ( it is too late here now and I did not checked the implementation with a fresh head. I thought that it is better to release current state for discussion as I likely won't be able to work on newapi for at least another week ) - use atomics in DefaultNode setInode/inode; see Lookup and corresponding description nearby DefaultNode.setInode for why it is needed. - inode.{lookupCount,nodeID} are now protected not by global rawBridge.mu, but instead by inode.mu . - change Node operation to return Nodes, not Inode. In particulare Node.Lookup should now return Node. Inodes are internal index of nodefs VFS (see package description) and we should not load filesystem implementations to think about them where we can. Also it makes a more closed interface when filesystem works in terms it nodes completely. Also this way we offload filesystems for caring about tricky details of how to create inode for a hardlinked entry (see Lookup for details which handles about it) - Remove Node.Inode -> nodefs.InodeOf(Node). this way there will be no possibility to override Node.Inode and we can specify InodeOf semantic exactly in API docs. - unlockNodes: sort is not needed - lock/unlock Nodes: avoid duplicates (e.g. there can be duplicates if dir/a and dir/b are hardlinks to the same file. If we don't avoid duplicates lockNodes will deadlock) - made some other edits, part of them not complete...
... | ... | @@ -6,22 +6,47 @@ package nodefs |
import ( | ||
"context" | ||
"sync/atomic" | ||
"time" | ||
"unsafe" | ||
"github.com/hanwen/go-fuse/fuse" | ||
) | ||
// DefaultNode must be embedded in a Node implementation. | ||
// DefaultNode provides common base Node functionality. | ||
// | ||
// It must be embedded in any Node implementation. | ||
type DefaultNode struct { | ||
inode *Inode | ||
} | ||
func (dn *DefaultNode) setInode(n *Inode) { | ||
dn.inode = n | ||
} | ||
func (dn *DefaultNode) Inode() *Inode { | ||
return dn.inode | ||
inode_ *Inode | ||
} | ||
// set/retrieve inode. | ||
// | ||
// node -> inode association, can be simultaneously tried to be set, if for e.g. | ||
|
||
// | ||
// root | ||
// / \ | ||
// dir1 dir2 | ||
// \ / | ||
// file | ||
// | ||
// dir1.Lookup("file") and dir2.Lookup("file") are executed simultaneously. | ||
// | ||
// We use atomics so that only one set can win and rawBridge.Lookup cares to | ||
// cancel inode that loosed. | ||
// | ||
// To read node.inode atomic.LoadPointer is used, however it is not expensive | ||
// since it translates to regular MOVQ on amd64. | ||
func (dn *DefaultNode) setInode(inode *Inode) bool { | ||
return atomic.CompareAndSwapPointer( | ||
(*unsafe.Pointer)(unsafe.Pointer(&dn.inode_)), | ||
nil, unsafe.Pointer(inode)) | ||
} | ||
func (dn *DefaultNode) inode() *Inode { | ||
return (*Inode)(atomic.LoadPointer( | ||
(*unsafe.Pointer)(unsafe.Pointer(&dn.inode_)))) | ||
} | ||
func (n *DefaultNode) Read(ctx context.Context, f File, dest []byte, off int64) (fuse.ReadResult, fuse.Status) { | ||
... | ... |
... | ... | @@ -34,38 +34,93 @@ type Inode struct { |
// Following data is mutable. | ||
// Protected by bridge.mu | ||
lookupCount uint64 | ||
nodeID uint64 | ||
// mu protects the following mutable fields. When locking | ||
// multiple Inodes, locks must be acquired using | ||
// lockNodes/unlockNodes | ||
mu sync.Mutex | ||
// incremented every time the 'children' or 'parents' field is changed. | ||
// changeCounter increments every time the below mutable state | ||
// (lookupCount, nodeID, children, parents) is modified. | ||
// | ||
// This is used in places where we have to relock inode into inode | ||
// group lock, and after locking the group we have to check if inode | ||
// did not changed, and if it changed - retry the operation. | ||
changeCounter uint32 | ||
lookupCount uint64 | ||
// ID of the inode; 0 if inode was forgotten. | ||
// forgotten inodes are unlinked from parent and children, but could be | ||
// still not yet removed from bridge.nodes . | ||
nodeID uint64 | ||
children map[string]*Inode | ||
parents map[parentData]struct{} | ||
} | ||
// newInode creates creates new inode pointing to node. | ||
// | ||
// node -> inode association is NOT set. | ||
// the inode is _not_ yet has | ||
func newInode(node Node, mode uint32) *Inode { | ||
inode := &Inode{ | ||
mode: mode ^ 07777, | ||
node: node, | ||
//bridge: n.bridge, | ||
parents: make(map[parentData]struct{}), | ||
} | ||
if mode&fuse.S_IFDIR != 0 { | ||
inode.children = make(map[string]*Inode) | ||
} | ||
//node.setInode(ch) | ||
return inode | ||
} | ||
// sortNodes rearranges inode group in consistent order. | ||
// | ||
// The nodes are ordered by their in-RAM address, which gives consistency | ||
// property: for any A and B inodes, sortNodes will either always order A < B, | ||
// or always order A > B. | ||
// | ||
// See lockNodes where this property is used to avoid deadlock when taking | ||
// locks on inode group. | ||
func sortNodes(ns []*Inode) { | ||
sort.Slice(ns, func(i, j int) bool { | ||
return uintptr(unsafe.Pointer(ns[i])) < uintptr(unsafe.Pointer(ns[j])) | ||
}) | ||
} | ||
// lockNodes locks group of inodes. | ||
// | ||
// It always lock the inodes in the same order - to avoid deadlocks. | ||
// It also avoids locking an inode more than once, if it was specified multiple times. | ||
// An example when an inode might be given multiple times is if dir/a and dir/b | ||
// are hardlinked to the same inode and the caller needs to take locks on dir children. | ||
// | ||
// It is valid to give nil nodes - those are simply ignored. | ||
|
||
func lockNodes(ns ...*Inode) { | ||
sortNodes(ns) | ||
var nprev *Inode | ||
for _, n := range ns { | ||
n.mu.Lock() | ||
if n != nprev { | ||
n.mu.Lock() | ||
nprev = n | ||
} | ||
} | ||
} | ||
// unlockNodes releases locks taken by lockNodes. | ||
func unlockNodes(ns ...*Inode) { | ||
// we don't need to unlock in the same order that was used in lockNodes. | ||
// however it still helps to have nodes sorted to avoid duplicates. | ||
sortNodes(ns) | ||
var nprev *Inode | ||
for _, n := range ns { | ||
n.mu.Unlock() | ||
if n != nprev { | ||
n.mu.Unlock() | ||
nprev = n | ||
} | ||
} | ||
} | ||
... | ... | @@ -74,9 +129,14 @@ func unlockNodes(ns ...*Inode) { |
// kernel has no way of reviving forgotten nodes by its own | ||
// initiative. | ||
func (n *Inode) Forgotten() bool { | ||
/* | ||
n.bridge.mu.Lock() | ||
defer n.bridge.mu.Unlock() | ||
return n.lookupCount == 0 | ||
*/ | ||
n.mu.Lock() | ||
defer n.mu.Unlock() | ||
return n.nodeID == 0 | ||
} | ||
// Node returns the Node object implementing the file system operations. | ||
... | ... | @@ -155,14 +215,24 @@ func (n *Inode) FindChildByOpaqueID(name string, opaqueID uint64) *Inode { |
return nil | ||
} | ||
func (n *Inode) addLookup(name string, child *Inode) { | ||
child.lookupCount++ | ||
child.parents[parentData{name, n}] = struct{}{} | ||
n.children[name] = child | ||
child.changeCounter++ | ||
n.changeCounter++ | ||
// setEntry does `iparent[name] = ichild` linking. | ||
// | ||
// setEntry must not be called simultaneously for any of iparent or ichild. | ||
// This, for example could be satisfied if both iparent and ichild are locked, | ||
// but it could be also valid if only iparent is locked and ichild was just | ||
// created and only one goroutine keeps referencing it. | ||
// | ||
// XXX also ichild.lookupCount++ ? | ||
func (iparent *Inode) setEntry(name string, ichild *Inode) { | ||
// ichild.lookupCount++ | ||
ichild.parents[parentData{name, iparent}] = struct{}{} | ||
iparent.children[name] = ichild | ||
ichild.changeCounter++ | ||
iparent.changeCounter++ | ||
} | ||
// XXX kill | ||
/* | ||
func (n *Inode) clearParents() { | ||
for { | ||
lockme := []*Inode{n} | ||
... | ... | @@ -191,7 +261,10 @@ func (n *Inode) clearParents() { |
} | ||
} | ||
} | ||
*/ | ||
// XXX kill | ||
/* | ||
func (n *Inode) clearChildren() { | ||
if n.mode != fuse.S_IFDIR { | ||
return | ||
... | ... | @@ -226,12 +299,18 @@ func (n *Inode) clearChildren() { |
} | ||
} | ||
// XXX not right - we cannot fully clear our children, because they can | ||
// be also children of another directory. | ||
|
||
// | ||
// XXX also not right - the kernel can send FORGET(idir) but keep | ||
|
||
// references to children inodes. | ||
for _, ch := range lockme { | ||
if ch != n { | ||
ch.clearChildren() | ||
} | ||
} | ||
} | ||
*/ | ||
// NewPersistentInode returns an Inode with a LookupCount == 1, ie. the | ||
// node will only get garbage collected if the kernel issues a forget | ||
... | ... |
-
Maintainer
.
-
mentioned in merge request nexedi/wendelin.core!15 (closed)