Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions pkg/raised/design_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
21 changes: 11 additions & 10 deletions pkg/raised/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 16 additions & 16 deletions pkg/raised/keying.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -228,20 +228,20 @@ func (self *sentinelErrorKeyer[T]) Key(err error) (ErrorKey, bool) {

}

func (self *sentinelErrorKeyer[T]) isErrorKeyer() bool {
func (self *sentinelErrorKeyer[S]) isErrorKeyer() bool {
return true
}

// keyCache is a timedCache mapping (code path, terminal cause string) to ErrorKey.
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
}
Expand Down
37 changes: 20 additions & 17 deletions pkg/raised/keying_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
}
Expand All @@ -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")
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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")
}
Expand All @@ -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")
}
Expand All @@ -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")
}
Expand All @@ -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")
}
Expand All @@ -371,26 +374,26 @@ 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)
}
}

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)
}
}

// ---- 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)
}
Expand Down
Loading