Add MISSING_FILE_GENERATION special value for "file does not exist"

Implements a new special generation counter value (UINT64_MAX) that
allows clients to assert that a file should NOT exist when performing
operations.

Primary use case: Atomic "create-if-not-exists" operations
When combined with TOASTY_WRITE_CREATE_IF_MISSING flag:
- File doesn't exist → server creates file and writes (SUCCESS)
- File exists → gen_match fails → BADGEN error (prevents overwrite)

This enables race-condition-free file creation where you want to
create a NEW file or fail if it already exists.

Key changes:
- Added MISSING_FILE_GENERATION constant to file_tree.h
- Updated gen_match() to return false when checking existing entities
  against MISSING_FILE_GENERATION
- Modified file_tree_delete_entity() to succeed (no-op) when file is
  missing and MISSING_FILE_GENERATION is specified (idempotent delete)
- Modified file_tree_write() to validate against the new value
- Added documentation in metadata_server.c explaining how
  MISSING_FILE_GENERATION works WITH CREATE_IF_MISSING
- Updated protocol documentation in PROTOCOL.txt
- Added comprehensive feature documentation in MISSING_FILE_GENERATION.md

Additional use cases:
- Idempotent delete operations (delete only if not already gone)
- Detecting unexpected file creation in validation/testing scenarios

The implementation is backward compatible and type-safe. Files are
never assigned MISSING_FILE_GENERATION as their actual generation value.
This commit is contained in:
Claude
2025-12-05 09:10:17 +01:00
committed by cozis
parent e73bb3a7d8
commit 6746b0d5fb
6 changed files with 85 additions and 11 deletions
+36 -8
View File
@@ -108,10 +108,17 @@ static void dir_free(Dir *d)
static bool gen_match(uint64_t expected_gen, uint64_t entity_gen)
{
assert(entity_gen != NO_GENERATION);
assert(entity_gen != MISSING_FILE_GENERATION);
// NO_GENERATION means "skip generation check"
if (expected_gen == NO_GENERATION)
return true;
// MISSING_FILE_GENERATION means "expect file to NOT exist"
// Since we're checking against an existing entity, this is a mismatch
if (expected_gen == MISSING_FILE_GENERATION)
return false;
return expected_gen == entity_gen;
}
@@ -350,11 +357,17 @@ int file_tree_delete_entity(FileTree *ft, string path,
return FILETREE_NOTDIR;
int i = dir_find(&e->d, comps[num_comps-1]);
if (i == -1)
if (i == -1) {
// File doesn't exist
// If caller expected it not to exist (MISSING_FILE_GENERATION), succeed
if (expected_gen == MISSING_FILE_GENERATION)
return 0;
return FILETREE_NOENT;
}
// File exists - check generation
if (dir_remove(&e->d, i, expected_gen) < 0)
return -1; // TODO: proper error code
return FILETREE_BADGEN;
return 0;
}
@@ -385,14 +398,21 @@ int file_tree_write(
Entity *e = resolve_path(&ft->root, comps, num_comps);
if (e == NULL)
if (e == NULL) {
// File doesn't exist
// If caller expected it not to exist (MISSING_FILE_GENERATION), that's correct
// but we still can't write to a non-existent file (need CREATE_IF_MISSING flag in client layer)
if (expect_gen == MISSING_FILE_GENERATION)
return FILETREE_NOENT; // Expected behavior: file missing as expected, but can't write
return FILETREE_NOENT;
}
if (e->is_dir)
return FILETREE_ISDIR;
// Check generation - will fail if expect_gen is MISSING_FILE_GENERATION (expects missing but file exists)
if (!gen_match(expect_gen, e->gen))
return -1; // TODO: proper error code
return FILETREE_BADGEN;
File *f = &e->f;
@@ -519,18 +539,26 @@ int file_tree_read(FileTree *ft, string path,
*actual_bytes = len;
}
if (len == 0)
if (len == 0) {
assert(e->gen != NO_GENERATION);
*gen = e->gen;
return 0;
}
uint64_t first_chunk_index = off / f->chunk_size;
uint64_t last_chunk_index = first_chunk_index + (len - 1) / f->chunk_size;
if (first_chunk_index >= f->num_chunks)
if (first_chunk_index >= f->num_chunks) {
*gen = e->gen;
return 0;
}
if (last_chunk_index >= f->num_chunks) {
if (f->num_chunks == 0)
if (f->num_chunks == 0) {
assert(e->gen != NO_GENERATION);
*gen = e->gen;
return 0;
}
last_chunk_index = f->num_chunks-1;
}
@@ -560,7 +588,7 @@ string file_tree_strerror(int code)
case FILETREE_EXISTS : return S("File or directory already exists");
case FILETREE_BADPATH: return S("Invalid path");
case FILETREE_BADOP : return S("Invalid operation");
case FILETREE_BADGEN : return S("Generation counter cannot be zero for write operations");
case FILETREE_BADGEN : return S("Generation counter mismatch or invalid value");
default:break;
}
return S("Unknown error");