Merge pull request 'Give each node a declaration-files bucket of its own, so a grant names it and never needs a wildcard (issue 1694, ADR 0488)' (#64) from fix/1694-bucket-per-node into main
This commit was merged in pull request #64.
This commit is contained in:
@@ -106,7 +106,7 @@ func TestAFailedWriteWaitsNoLongerThanTheDeadline(t *testing.T) {
|
||||
default:
|
||||
}
|
||||
})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
_, err = Ask(ctx, socket, aLineTheSocketCannotHold())
|
||||
@@ -117,7 +117,7 @@ func TestAFailedWriteWaitsNoLongerThanTheDeadline(t *testing.T) {
|
||||
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
t.Fatalf("the error is not the deadline: %v", err)
|
||||
}
|
||||
if waited > 3*time.Second {
|
||||
t.Fatalf("waited %s with a deadline of 1s", waited)
|
||||
if waited > time.Second {
|
||||
t.Fatalf("waited %s with a deadline of 50ms", waited)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ type File struct {
|
||||
|
||||
// Digest and Size say the content is not carried but fetched: a file over FileReferenceThreshold travels as
|
||||
// the sha256 digest of its content, "sha256:<64 hex>", and its size in bytes, in place of Content or Bytes
|
||||
// (novox/hq ADR 0486 §1). The node-engine fetches the object of that name from FileBucket, checks what it
|
||||
// (novox/hq ADR 0486 §1). The node-engine fetches the object of that name from FileBucketFor(node), checks what it
|
||||
// received against Digest and Size, and only then writes the file. Exclusive with Content, Sealed, Bytes,
|
||||
// VaultSealed and Secrets; sent only to an engine that reports FileReferencesCapability, as an older engine
|
||||
// refuses a field it does not know, whole.
|
||||
|
||||
@@ -14,13 +14,38 @@ const (
|
||||
// FileReferenceThreshold is the size above which a file travels by digest: a file of this many bytes or fewer
|
||||
// stays in the declaration as it is.
|
||||
FileReferenceThreshold = 64 * 1024
|
||||
// FileBucket is the object bucket of the bus that holds the contents, each under the hex of its sha256 digest
|
||||
// (ObjectName). The controller alone writes it; a node-engine reads an object by name.
|
||||
FileBucket = "declaration-files"
|
||||
// FileBucketPrefix starts the name of a node's object bucket, FileBucketFor(node), which holds the contents of the
|
||||
// files of that node's declaration, each under the hex of its sha256 digest (ObjectName). One bucket per node,
|
||||
// so a grant names it and a literal `*` is never needed (ADR 0486 §2, changed by ADR 0488 §1 and §2). The controller alone writes it; the
|
||||
// node's engine reads an object by name.
|
||||
FileBucketPrefix = "declaration-files-"
|
||||
// FileStreamPrefix starts the name of the stream behind such a bucket, which the bus's grants name.
|
||||
FileStreamPrefix = "OBJ_"
|
||||
// FileReferencesCapability is what a node-engine reports when it reads a file given as a digest and size.
|
||||
FileReferencesCapability = "file-references"
|
||||
)
|
||||
|
||||
var nodeToken = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||
|
||||
// FileBucketFor is the name of the object bucket of a node's files, "declaration-files-<node>" (novox/hq ADR 0488
|
||||
// §4, which replaces the single bucket constant of issue 1694). The node name must be a name the bus accepts in a
|
||||
// bucket and in a subject token: letters, digits, '_' and '-', so no '.', '*', '>' or space.
|
||||
func FileBucketFor(node string) (string, error) {
|
||||
if !nodeToken.MatchString(node) {
|
||||
return "", fmt.Errorf("node %q; a node's bucket is named by letters, digits, '_' and '-' only", node)
|
||||
}
|
||||
return FileBucketPrefix + node, nil
|
||||
}
|
||||
|
||||
// FileStreamFor is the name of the stream behind a node's bucket, "OBJ_" and the bucket's name.
|
||||
func FileStreamFor(node string) (string, error) {
|
||||
b, err := FileBucketFor(node)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return FileStreamPrefix + b, nil
|
||||
}
|
||||
|
||||
var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
||||
|
||||
// DigestOf is the digest a reference names for a content: "sha256:<64 lower-case hex>".
|
||||
|
||||
@@ -127,10 +127,32 @@ func TestWhichFilesTravelByDigest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTheSharedNames(t *testing.T) {
|
||||
if FileBucket != "declaration-files" || FileReferencesCapability != "file-references" || FileReferenceThreshold != 65536 {
|
||||
if FileBucketPrefix != "declaration-files-" || FileReferencesCapability != "file-references" || FileReferenceThreshold != 65536 {
|
||||
t.Fatal("a name changed")
|
||||
}
|
||||
if _, err := ObjectName("sha256:zz"); err == nil {
|
||||
t.Fatal("bad digest named an object")
|
||||
}
|
||||
}
|
||||
|
||||
// 1694/AC6: each node has a bucket of its own, so a grant names it and never needs a `*` (ADR 0486 §2).
|
||||
func TestEachNodeHasABucketOfItsOwn(t *testing.T) {
|
||||
for _, n := range []string{"ace", "g14", "novox", "shanks"} {
|
||||
b, err := FileBucketFor(n)
|
||||
if err != nil || b != "declaration-files-"+n {
|
||||
t.Errorf("bucket for %q: %q %v", n, b, err)
|
||||
}
|
||||
s, err := FileStreamFor(n)
|
||||
if err != nil || s != "OBJ_declaration-files-"+n {
|
||||
t.Errorf("stream for %q: %q %v", n, s, err)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"", "a.b", "a*", "*", ">", "a b", "a>", "a/b", "ä"} {
|
||||
if _, err := FileBucketFor(bad); err == nil {
|
||||
t.Errorf("bucket for %q accepted", bad)
|
||||
}
|
||||
if _, err := FileStreamFor(bad); err == nil {
|
||||
t.Errorf("stream for %q accepted", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +138,11 @@ func text(t *testing.T, a map[string]any) (string, bool) {
|
||||
return content[0].(map[string]any)["text"].(string), isError
|
||||
}
|
||||
|
||||
// Two writers incrementing one counter a thousand times each end at two thousand: every number handed out
|
||||
// Two writers incrementing one counter two hundred times each end at four hundred: every number handed out
|
||||
// once, none lost, each writer's numbers rising — read, then written only at the revision read.
|
||||
func TestTwoWritersIncrementingOneCounterLoseNoUpdate(t *testing.T) {
|
||||
r := &casRuntime{keys: map[string]StateEntry{}, writes: map[string]int{}}
|
||||
const each = 1000
|
||||
const each = 200 // enough to collide every run (about a third of writes are lost), cheap on a slow builder
|
||||
got := counting(t, r, func() (any, error) {
|
||||
var wg sync.WaitGroup
|
||||
numbers := make([][]uint64, 2)
|
||||
@@ -179,7 +179,7 @@ func TestTwoWritersIncrementingOneCounterLoseNoUpdate(t *testing.T) {
|
||||
}
|
||||
return len(seen), nil
|
||||
})
|
||||
if out, isError := text(t, got); isError || out != "2000" {
|
||||
if out, isError := text(t, got); isError || out != fmt.Sprint(2*each) {
|
||||
t.Fatalf("two writers counted %s (error: %v)", out, isError)
|
||||
}
|
||||
var final uint64
|
||||
@@ -190,7 +190,7 @@ func TestTwoWritersIncrementingOneCounterLoseNoUpdate(t *testing.T) {
|
||||
if r.lost == 0 {
|
||||
t.Fatal("the two writers never collided: the test proved nothing about contention")
|
||||
}
|
||||
t.Logf("2000 increments, %d writes lost to the other writer and tried again", r.lost)
|
||||
t.Logf("%d increments, %d writes lost to the other writer and tried again", 2*each, r.lost)
|
||||
if r.writes["create"] != 1 || r.writes["update"] != 2*each-1 || r.writes["put"] != 0 {
|
||||
t.Fatalf("written by %v: one create, then updates at the revision read, never a put", r.writes)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user