diff --git a/pkg/raised/design_test.go b/pkg/raised/design_test.go index 5873d0e..9446add 100644 --- a/pkg/raised/design_test.go +++ b/pkg/raised/design_test.go @@ -10,6 +10,8 @@ import ( rce "github.com/pkg/errors" // imported to allow comparing with raised... ) +type S = *Sentinel[pkg] + // ==================================================================================== func TestTmp_Errorf(t *testing.T) { @@ -337,7 +339,7 @@ func BenchmarkDesign_tracePCCaches256r16(b *testing.B) { func BenchmarkDesign_keyRaised_Stables64r4(b *testing.B) { - ek, fail := NewErrorKeyer[pkg](nil) + ek, fail := NewErrorKeyer[S](nil) if nil != fail { b.Fatalf("failed instantiating ErrorKeyer, got error %v", fail) } @@ -351,7 +353,7 @@ func BenchmarkDesign_keyRaised_Stables64r4(b *testing.B) { func BenchmarkDesign_keyRaised_Stables64r8(b *testing.B) { - ek, fail := NewErrorKeyer[pkg](nil) + ek, fail := NewErrorKeyer[S](nil) if nil != fail { b.Fatalf("failed instantiating ErrorKeyer, got error %v", fail) } @@ -365,7 +367,7 @@ func BenchmarkDesign_keyRaised_Stables64r8(b *testing.B) { func BenchmarkDesign_keyRaised_Stables64r16(b *testing.B) { - ek, fail := NewErrorKeyer[pkg](nil) + ek, fail := NewErrorKeyer[S](nil) if nil != fail { b.Fatalf("failed instantiating ErrorKeyer, got error %v", fail) } @@ -379,7 +381,7 @@ func BenchmarkDesign_keyRaised_Stables64r16(b *testing.B) { func BenchmarkDesign_keyRaised_Unstables64r4(b *testing.B) { - ek, fail := NewErrorKeyer[pkg](nil) + ek, fail := NewErrorKeyer[S](nil) if nil != fail { b.Fatalf("failed instantiating ErrorKeyer, got error %v", fail) } @@ -400,7 +402,7 @@ func BenchmarkDesign_keyRaised_Unstables64r4(b *testing.B) { func BenchmarkDesign_keyRaised_Unstables64r8(b *testing.B) { - ek, fail := NewErrorKeyer[pkg](nil) + ek, fail := NewErrorKeyer[S](nil) if nil != fail { b.Fatalf("failed instantiating ErrorKeyer, got error %v", fail) } @@ -421,7 +423,7 @@ func BenchmarkDesign_keyRaised_Unstables64r8(b *testing.B) { func BenchmarkDesign_keyRaised_Unstables64r16(b *testing.B) { - ek, fail := NewErrorKeyer[pkg](nil) + ek, fail := NewErrorKeyer[S](nil) if nil != fail { b.Fatalf("failed instantiating ErrorKeyer, got error %v", fail) } diff --git a/pkg/raised/doc.go b/pkg/raised/doc.go index 58c6cd4..88923bc 100644 --- a/pkg/raised/doc.go +++ b/pkg/raised/doc.go @@ -83,19 +83,20 @@ // root cause. This is useful for error aggregation, deduplication, and // monitoring. // -// An ErrorKeyer is scoped to the sentinel family T, consistent with the phantom -// type used for sentinel declaration: +// An ErrorKeyer is scoped to a Sentinel type S // -// type pkg struct {} +// type pkg struct {} // phantom type // -// var Keyer, _ = raised.NewErrorKeyer[pkg](nil) +// type S = raised.Sentinel[pkg] // your own Sentinel type // -// func handle(err error) { -// key, ok := Keyer.Key(err) -// if ok { -// monitor.Record(key) -// } -// } +// var Keyer, _ = raised.NewErrorKeyer[*S](nil) +// +// func handle(err error) { +// key, ok := Keyer.Key(err) +// if ok { +// monitor.Record(key) +// } +// } // // The ErrorKey is stable across process restarts and hosts as long as the // source code has not changed — it is derived from file/line strings rather diff --git a/pkg/raised/keying.go b/pkg/raised/keying.go index 8a68215..ec75397 100644 --- a/pkg/raised/keying.go +++ b/pkg/raised/keying.go @@ -39,10 +39,10 @@ type ErrorKeyer interface { } // NewErrorKeyer returns an ErrorKeyer using SHA256 as the default hash function, -// scoped to the sentinel family identified by the phantom type T. -func NewErrorKeyer[T any](ukl UnstableKeyListener) (ErrorKeyer, error) { +// scoped to the sentinel type S. +func NewErrorKeyer[S SentinelError](ukl UnstableKeyListener) (ErrorKeyer, error) { // sha256 should provide better collision resistance than fnv128 - return NewSentinelErrorKeyer[T](sha256.New, ukl) + return NewSentinelErrorKeyer[S](sha256.New, ukl) } // UnstableKeyEvent is delivered to an UnstableKeyListener when the ErrorKey @@ -91,7 +91,7 @@ func (self UnstableKeyListenerFunc) OnUnstableKey(evt UnstableKeyEvent) { // sentinelErrorKeyer is the ErrorKeyer implementation scoped to sentinel family T. // It is immutable after construction. -type sentinelErrorKeyer[T any] struct { +type sentinelErrorKeyer[S SentinelError] struct { // hf is the hash factory used to compute ErrorKeys. hf HashFunc @@ -103,10 +103,10 @@ type sentinelErrorKeyer[T any] struct { ukl UnstableKeyListener } -// NewSentinelErrorKeyer returns a ErrorKeyer scoped to the sentinel family identified -// by the phantom type T, using hf as the hash function. +// NewSentinelErrorKeyer returns a ErrorKeyer scoped to the sentinel S type, using hf as +// the hash function. // Returns ErrInvalidHash if hf is nil or produces fewer than keySize bytes. -func NewSentinelErrorKeyer[T any](hf HashFunc, ukl UnstableKeyListener) (ErrorKeyer, error) { +func NewSentinelErrorKeyer[S SentinelError](hf HashFunc, ukl UnstableKeyListener) (ErrorKeyer, error) { // validate hf if nil == hf { return nil, Trace(ErrInvalidHash, "nil hash function") @@ -116,17 +116,17 @@ func NewSentinelErrorKeyer[T any](hf HashFunc, ukl UnstableKeyListener) (ErrorKe return nil, Tracef(ErrInvalidHash, "insufficient hash size %d < %d", h.Size(), keySize) } - sk := sentinelErrorKeyer[T]{hf: hf, tc: &keyCache{clock: ticks}, ukl: ukl} + sk := sentinelErrorKeyer[S]{hf: hf, tc: &keyCache{clock: ticks}, ukl: ukl} return &sk, nil } // Key computes a stable ErrorKey for err. err must be a raised Error produced // by Trace. The key is derived from the error's propagation path -// (as file/line strings) and the terminal cause resolved via UnwrapTerminal[T]. +// (as file/line strings) and the terminal cause resolved via UnwrapTerminal[S]. // Results are cached by code path and terminal cause string. // Returns false if err is not a raised Error or has no resolvable terminal cause. -func (self *sentinelErrorKeyer[T]) Key(err error) (ErrorKey, bool) { +func (self *sentinelErrorKeyer[S]) Key(err error) (ErrorKey, bool) { erk := ErrorKey{} // abort if err is not an *errTrace @@ -139,7 +139,7 @@ func (self *sentinelErrorKeyer[T]) Key(err error) (ErrorKey, bool) { ert.snapshot(&snp) // extract ert root cause (aka terminal) - trm := UnwrapTerminal[T](&snp) + trm := UnwrapTerminal[S](&snp) if nil == trm { return erk, false } @@ -228,7 +228,7 @@ func (self *sentinelErrorKeyer[T]) Key(err error) (ErrorKey, bool) { } -func (self *sentinelErrorKeyer[T]) isErrorKeyer() bool { +func (self *sentinelErrorKeyer[S]) isErrorKeyer() bool { return true } @@ -236,12 +236,12 @@ func (self *sentinelErrorKeyer[T]) isErrorKeyer() bool { type keyCache = timedCache[L1Key, string, ErrorKey] // UnwrapTerminal returns "minimal" error obtained by recursively unwrapping err or -// casting err to Sentinel[T], SentinelError... -func UnwrapTerminal[T any](err error) error { +// casting err to S, SentinelError... +func UnwrapTerminal[S SentinelError](err error) error { - // check if err wraps a Sentinel[T] + // check if err wraps a S // if yes uses it as err Cause - s := new(Sentinel[T]) + var s S if errors.As(err, &s) { return s } diff --git a/pkg/raised/keying_test.go b/pkg/raised/keying_test.go index 823ec77..7ec827a 100644 --- a/pkg/raised/keying_test.go +++ b/pkg/raised/keying_test.go @@ -16,6 +16,9 @@ type familyB struct{} // ---- sentinel declarations ---- +type Sa = *Sentinel[familyA] +type Sb = *Sentinel[familyB] + var ( errA1 = NewSentinelError[familyA]("ERROR(1) sentinel A1") errA2 = NewSentinelError[familyA]("ERROR(2) sentinel A2") @@ -25,7 +28,7 @@ var ( // ---- construction tests ---- func TestKeying_NewSentinelErrorKeyer_NilHash(t *testing.T) { - _, err := NewSentinelErrorKeyer[familyA](nil, nil) + _, err := NewSentinelErrorKeyer[Sa](nil, nil) if err == nil { t.Fatal("expected error for nil hash function, got nil") } @@ -35,7 +38,7 @@ func TestKeying_NewSentinelErrorKeyer_NilHash(t *testing.T) { } func TestKeying_NewSentinelErrorKeyer_InsufficientHashSize(t *testing.T) { - _, err := NewSentinelErrorKeyer[familyA](smallHashFactory, nil) + _, err := NewSentinelErrorKeyer[Sa](smallHashFactory, nil) if err == nil { t.Fatal("expected error for insufficient hash size, got nil") } @@ -45,7 +48,7 @@ func TestKeying_NewSentinelErrorKeyer_InsufficientHashSize(t *testing.T) { } func TestKeying_NewSentinelErrorKeyer_Valid(t *testing.T) { - ek, err := NewSentinelErrorKeyer[familyA](sha256Factory, nil) + ek, err := NewSentinelErrorKeyer[Sa](sha256Factory, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -58,7 +61,7 @@ func TestKeying_NewSentinelErrorKeyer_Valid(t *testing.T) { } func TestKeying_NewErrorKeyer_Valid(t *testing.T) { - ek, err := NewErrorKeyer[familyA](nil) + ek, err := NewErrorKeyer[Sa](nil) if err != nil { t.Fatalf("NewErrorKeyer: unexpected error: %v", err) } @@ -232,8 +235,8 @@ func TestKeying_Key_ForeignErrorLeafHeuristic_Stable(t *testing.T) { func TestKeying_Key_CrossFamilySentinels_NotEqual(t *testing.T) { // errA1 and errB1 share the same ERROR code but different phantom families, // so their Fingerprints differ and the keys must differ. - ekA, _ := NewErrorKeyer[familyA](nil) - ekB, _ := NewErrorKeyer[familyB](nil) + ekA, _ := NewErrorKeyer[Sa](nil) + ekB, _ := NewErrorKeyer[Sb](nil) kA, okA := ekA.Key(traceA(errA1)) kB, okB := ekB.Key(traceA(errB1)) @@ -270,7 +273,7 @@ func TestKeying_Key_CacheHit_ConsistentResult(t *testing.T) { } func TestKeying_Key_NilListener_NoPanic(t *testing.T) { - ek, err := NewSentinelErrorKeyer[familyA](sha256Factory, nil) + ek, err := NewSentinelErrorKeyer[Sa](sha256Factory, nil) if err != nil { t.Fatalf("NewSentinelErrorKeyer: %v", err) } @@ -283,7 +286,7 @@ func TestKeying_Key_NilListener_NoPanic(t *testing.T) { func TestKeying_Key_UnstableKeyListener_Called(t *testing.T) { listener := &recordingListener{} - ek, err := NewSentinelErrorKeyer[familyA](sha256Factory, listener) + ek, err := NewSentinelErrorKeyer[Sa](sha256Factory, listener) if err != nil { t.Fatalf("NewSentinelErrorKeyer: %v", err) } @@ -319,7 +322,7 @@ func TestKeying_Key_UnstableKeyListener_Called(t *testing.T) { func TestKeying_UnwrapTerminal_SentinelT(t *testing.T) { // errA1 is *Sentinel[familyA]; errors.As should match it immediately. wrapped := fmt.Errorf("outer: %w", errA1) - got := UnwrapTerminal[familyA](wrapped) + got := UnwrapTerminal[Sa](wrapped) if got == nil { t.Fatal("expected non-nil terminal") } @@ -330,9 +333,9 @@ func TestKeying_UnwrapTerminal_SentinelT(t *testing.T) { func TestKeying_UnwrapTerminal_SentinelError(t *testing.T) { // errB1 is *Sentinel[familyB]: satisfies SentinelError but not Sentinel[familyA]. - // UnwrapTerminal[familyA] should fall through to the SentinelError branch. + // UnwrapTerminal[Sa] should fall through to the SentinelError branch. wrapped := fmt.Errorf("outer: %w", errB1) - got := UnwrapTerminal[familyA](wrapped) + got := UnwrapTerminal[Sa](wrapped) if got == nil { t.Fatal("expected non-nil terminal") } @@ -345,7 +348,7 @@ func TestKeying_UnwrapTerminal_PlainError_DeepestLeaf(t *testing.T) { leaf := errors.New("leaf") outer := fmt.Errorf("outer: %w", fmt.Errorf("middle: %w", leaf)) - got := UnwrapTerminal[familyA](outer) + got := UnwrapTerminal[Sa](outer) if got == nil { t.Fatal("expected non-nil terminal") } @@ -361,7 +364,7 @@ func TestKeying_UnwrapTerminal_MultiBranchUnwrap(t *testing.T) { leafY := errors.New("leaf-Y") joined := errors.Join(leafX, leafY) - got := UnwrapTerminal[familyA](joined) + got := UnwrapTerminal[Sa](joined) if got == nil { t.Fatal("expected non-nil terminal for joined errors") } @@ -371,7 +374,7 @@ func TestKeying_UnwrapTerminal_MultiBranchUnwrap(t *testing.T) { } func TestKeying_UnwrapTerminal_Nil(t *testing.T) { - got := UnwrapTerminal[familyA](nil) + got := UnwrapTerminal[Sa](nil) if got != nil { t.Errorf("expected nil for nil input, got %v", got) } @@ -379,7 +382,7 @@ func TestKeying_UnwrapTerminal_Nil(t *testing.T) { func TestKeying_UnwrapTerminal_DirectSentinelT(t *testing.T) { // Passing errA1 directly (not wrapped): should be returned as-is. - got := UnwrapTerminal[familyA](errA1) + got := UnwrapTerminal[Sa](errA1) if got != errA1 { t.Errorf("expected errA1 returned directly, got %v", got) } @@ -387,10 +390,10 @@ func TestKeying_UnwrapTerminal_DirectSentinelT(t *testing.T) { // ---- helpers ---- -// mustKeyer creates a sentinelErrorKeyer[familyA] or fails the test. +// mustKeyer creates a sentinelErrorKeyer[Sa] or fails the test. func mustKeyer(t *testing.T, ukl UnstableKeyListener) ErrorKeyer { t.Helper() - ek, err := NewErrorKeyer[familyA](ukl) + ek, err := NewErrorKeyer[Sa](ukl) if err != nil { t.Fatalf("NewErrorKeyer: unexpected error: %v", err) }