This document describes the atomicity of (writing) decomposedfs operations by listing the relevant steps that happen when doing the according operations, highlighting potential problems with concurrent operations and describing the negative effects.
Several concurrent CreateDir
calls can get past the exit critera step 1 because the directory does not exist yet.
Each of the calls generates a new ID, creates the according node on disk and tries to link it to the parent.
Only the first one succeeds in that, the later ones fail because the link already exists (See
Considerations > Creating symlinks
).
Failing calls will leave an orphaned node behind (See reva issue #1601).
No risk of inconsistency.
See CreateDir
.
See CreateDir
.
There is no exit critera step so all concurrent calls try to create a symlink in step 3 with only one of them
succeeding (See Considerations > Creating symlinks
).
No risk of inconsistency.
Several concurrent calls can get past the exit criteria steps 1 and 2. But the first writing operation is always the actual move of the node on the filesystem which is an atomic filesystem operation. That means that with concurrent calls only one can ever succeed.
None.
Retrieving the existing node id happens when the upload is started in step 1. If no node is found at that point in time a new uuid will be assigned later on in step 3. Making the node visible to other uploads only happens when linking the node to its parent in step 4 though.
That means that when an upload starts while another one is still running for the same target path they will create and write different nodes for the same path and both upload the data to the blobstore.
With concurrent uploads the last one “wins” by deleting the link from the previous upload and then linking its node in the owner directory. The others leave orphaned nodes and blobs behind. These uploads seem to have succeeded but their data is essentially lost, they are NOT made available as old versions (See reva issue #1626).
Moving the file away in step 2 can interfere with concurrent operations. Another problem exists with step 4 happening concurrently as the different operations overwrite existing attributes one by one instead of writing the whole set of attributes atomically.
Concurrent operations compete with a chance of the others failing ungracefully. It can even happen that the extended attributes of two revisions are mixed in the resulting node (See reva issue #1627).
Only one of the concurrent operations can succeed with step 1 (See Considerations > Creating symlinks
).
None.
None.
None.
Symlinks are created using the os.Symlink
function. This function fails if the link already exists. Subsequent
operations are thus guaranteed not to replace a link that has alrady been created.
Example code showing showing this behavior:
package main
import (
"fmt"
"os"
)
func main() {
err := os.WriteFile("file1", []byte(""), 0600)
if err != nil {
os.Exit(1)
}
err = os.WriteFile("file2", []byte(""), 0600)
if err != nil {
os.Exit(1)
}
// Create first symlink
err = os.Symlink("file1", "link")
if err != nil {
os.Exit(1)
}
// Try to create symlink, expect EEXISTS
err = os.Symlink("file2", "link")
if err == nil {
os.Exit(1)
} else {
fmt.Println(err.Error())
fmt.Println("Success")
}
}
Files are renamed using the os.Rename
function. This function does not fail if it’s a file being renamed and the
target already exists. Instead the target is being replaced. Example code:
package main
import (
"fmt"
"os"
)
func main() {
err := os.WriteFile("file1", []byte(""), 0600)
if err != nil {
os.Exit(1)
}
err = os.WriteFile("file2", []byte(""), 0600)
if err != nil {
os.Exit(1)
}
// Overwrite file1 by renaming file2 file, expect no error
err = os.Rename("file2", "file1")
if err != nil {
os.Exit(1)
} else {
fmt.Println("Success")
}
}