diff --git a/annotation.go b/annotation.go new file mode 100644 index 00000000..9ef15f33 --- /dev/null +++ b/annotation.go @@ -0,0 +1,39 @@ +/* +© 2020–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package parl + +import ( + "fmt" + + "github.com/haraldrudell/parl/pruntime" +) + +const ( + // counts the stack-frame of [parl.Annotation] + parlAnnotationFrames = 1 + // counts the stack-frame of [parl.getAnnotation] + getAnnotationFrames = 1 +) + +// Annotation provides a default reovered-panic code annotation +// - “Recover from panic in mypackage.MyFunc” +// - [base package].[function]: "mypackage.MyFunc" +func Annotation() (a string) { + return getAnnotation(parlAnnotationFrames) +} + +// getAnnotation provides a default reovered-panic code getAnnotation +// - frames = 0 means immediate caller of getAnnotation +// - “Recover from panic in mypackage.MyFunc” +// - [base package].[function]: "mypackage.MyFunc" +func getAnnotation(frames int) (a string) { + if frames < 0 { + frames = 0 + } + return fmt.Sprintf("Recover from panic in %s:", + pruntime.NewCodeLocation(frames+getAnnotationFrames).PackFunc(), + ) +} diff --git a/awaitable-ch.go b/awaitable-ch.go index 39602a2c..eff31a57 100644 --- a/awaitable-ch.go +++ b/awaitable-ch.go @@ -5,7 +5,19 @@ All rights reserved package parl -// AwaitableCh is a channel whose only allowed operation is channel receive +// AwaitableCh is a one-to-many inter-thread wait-mechanic with happens-before // - AwaitableCh implements a semaphore +// - implementation is a channel whose only allowed operation is channel receive // - AwaitableCh transfers no data, instead channel close is the significant event +// +// Usage: +// +// <-ch // waits for event +// +// select { +// case <-ch: +// hasHappened = true +// default: +// hasHappened = false +// } type AwaitableCh <-chan struct{} diff --git a/awaitable.go b/awaitable.go index a0f3d448..35bb0681 100644 --- a/awaitable.go +++ b/awaitable.go @@ -12,10 +12,9 @@ import "sync/atomic" // - one-to-many, happens-before // - the synchronization mechanic is closing channel, allowing consumers to await // multiple events -// - status can be inspected in a thread-safe manner: isClosed, isAboutToClose -// allows for race-free consumers -// - Close is idempotent, panic-free -// - if atomic.Pointer[Awaitable] is used for retrieval, a cyclic semaphore is achieved +// - IsClosed provides thread-safe observability +// - Close is idempotent and panic-free +// - [parl.CyclicAwaitable] is re-armable, cyclic version type Awaitable struct { isClosed atomic.Bool ch chan struct{} @@ -33,18 +32,13 @@ func (a *Awaitable) Ch() (ch AwaitableCh) { // isClosed inspects whether the awaitable has been triggered // - isClosed indicates that the channel is closed -// - isAboutToClose indicates that Close has been invoked, -// but that channel close may still be in progress -// - if isClosed is true, isAboutToClose is also true -// - the two values are requried to attain race-free consumers // - Thread-safe -func (a *Awaitable) IsClosed() (isClosed, isAboutToClose bool) { +func (a *Awaitable) IsClosed() (isClosed bool) { select { case <-a.ch: isClosed = true default: } - isAboutToClose = a.isClosed.Load() return } diff --git a/channel-send.go b/channel-send.go index 06ab7356..c1e37600 100644 --- a/channel-send.go +++ b/channel-send.go @@ -49,7 +49,7 @@ func ChannelSend[T any](ch chan<- T, value T, nonBlocking ...bool) (didSend, isN // - the only way to determine closed channel is to send, which panics // - a separate function to recover the panic func channelSend[T any](ch chan<- T, value T, sendNb bool) (didSend bool, err error) { - defer Recover(Annotation(), &err, NoOnError) + defer PanicToErr(&err) // send non-blocking if sendNb { diff --git a/closable-chan.go b/closable-chan.go index eee9e2da..4ceff3aa 100644 --- a/closable-chan.go +++ b/closable-chan.go @@ -6,7 +6,6 @@ ISC License package parl import ( - "sync" "sync/atomic" ) @@ -29,14 +28,23 @@ import ( // defer errCh.Close(&err) // will not terminate the process // errCh.Ch() <- err type ClosableChan[T any] struct { - hasChannel atomic.Bool // hasChannel provides thread-safe lock-free read of ch - chLock sync.Mutex - // ch is the channel object - // - outside the new function, ch is written behind chLock - ch chan T - - isCloseInvoked atomic.Bool // indicates the channel being closed or about to close - closeOnce Once // [parl.Once] is an observable sync.Once + // ch0 is the channel object + // - ability to initialize ch0 in the constructor + // - ability to update ch0 after creation + // - ch0 therefore must be pointer + // - ch0 must offer thread-safe access and update + + // ch0 as provided by contructor or nil + ch0 chan T + // ch0 provided post-constructor because ch0 nil + chp atomic.Pointer[chan T] + + // indicates the channel about to close or closed + // - because the channel may transfer data, it cannot be inspected for being closed + isCloseInvoked atomic.Bool + // [parl.Once] is an observable sync.Once + // - indicates that the channel is closed + closeOnce Once } // NewClosableChan returns a channel with idempotent panic-free observable close @@ -44,40 +52,43 @@ type ClosableChan[T any] struct { // - if ch is not present, an unbuffered channel will be created // - cannot use lock in new function // - if an unbuffered channel is used, NewClosableChan is not required -func NewClosableChan[T any](ch ...chan T) (cl *ClosableChan[T]) { - c := ClosableChan[T]{} +func NewClosableChan[T any](ch ...chan T) (closable *ClosableChan[T]) { + var ch0 chan T if len(ch) > 0 { - if c.ch = ch[0]; c.ch != nil { - c.hasChannel.Store(true) - } + ch0 = ch[0] // if ch is present, apply it } - return &c + return &ClosableChan[T]{ch0: ch0} } -// Ch retrieves the channel. Thread-safe +// Ch retrieves the channel as bi-directional. Thread-safe // - nil is never returned -// - the channel may already be closed -// - do not close the channel other than using the Close method +// - the channel may be closed, use IsClosed to determine +// - do not close the channel other than using Close method // - per Go channel close, if one thread is blocked in channel send // while another thread closes the channel, a data race occurs +// - thread-safe solution is to set an additional indicator of +// close requested and then reading the channel which +// releases the sending thread func (c *ClosableChan[T]) Ch() (ch chan T) { return c.getCh() } -// ReceiveCh retrieves the channel. Thread-safe +// ReceiveCh retrieves the channel as receive-only. Thread-safe // - nil is never returned // - the channel may already be closed -// - do not close the channel other than using the Close method func (c *ClosableChan[T]) ReceiveCh() (ch <-chan T) { return c.getCh() } -// SendCh retrieves the channel. Thread-safe +// SendCh retrieves the channel as send-only. Thread-safe // - nil is never returned // - the channel may already be closed // - do not close the channel other than using the Close method // - per Go channel close, if one thread is blocked in channel send // while another thread closes the channel, a data race occurs +// - thread-safe solution is to set an additional indicator of +// close requested and then reading the channel which +// releases the sending thread func (c *ClosableChan[T]) SendCh() (ch chan<- T) { return c.getCh() } @@ -98,11 +109,12 @@ func (c *ClosableChan[T]) IsClosed(includePending ...bool) (isClosed bool) { // Close ensures the channel is closed // - Close does not return until the channel is closed. // - thread-safe panic-free deferrable observable -// - all invocations have close result in err +// - all invocations have the same close result in err // - didClose indicates whether this invocation closed the channel // - if errp is non-nil, it will receive the close result // - per Go channel close, if one thread is blocked in channel send // while another thread closes the channel, a data race occurs +// - thread-safe, panic-free, deferrable, idempotent func (cl *ClosableChan[T]) Close(errp ...*error) (didClose bool, err error) { // ensure isCloseInvoked true: channel is about to close @@ -131,29 +143,27 @@ func (cl *ClosableChan[T]) Close(errp ...*error) (didClose bool, err error) { } // getCh gets or initializes the channel object [ClosableChan.ch] -func (cl *ClosableChan[T]) getCh() (ch chan T) { - - // wrap lock in performance-friendly atomic - // - by reading hasChannel cl.ch access is thread-safe - // - if channel is closed, return whatever ch is - if cl.hasChannel.Load() || cl.closeOnce.IsDone() { - return cl.ch +func (c *ClosableChan[T]) getCh() (ch chan T) { + if ch = c.ch0; ch != nil { + return // channel from constructor return } - - // ensure a channel is present - cl.chLock.Lock() - defer cl.chLock.Unlock() - - if ch = cl.ch; ch == nil { - ch = make(chan T) - cl.ch = ch - cl.hasChannel.Store(true) + for { + if chp := c.chp.Load(); chp != nil { + ch = *chp + return // chp was present return + } + if ch == nil { + ch = make(chan T) + } + if c.chp.CompareAndSwap(nil, &ch) { + return // chp updated return + } } - return } // doClose is behind [ClosableChan.closeOnce] and // is therefore only invoked once +// - separate function because provided to Once func (cl *ClosableChan[T]) doClose() (err error) { // ensure a channel exists and close it diff --git a/closer.go b/closer.go index 28e87f60..8f1d7f30 100644 --- a/closer.go +++ b/closer.go @@ -20,7 +20,7 @@ const ( // Closer handles panics. // if errp is non-nil, panic values updates it using errors.AppendError. func Closer[T any](ch chan T, errp *error) { - defer Recover(Annotation(), errp, NoOnError) + defer PanicToErr(errp) close(ch) } @@ -29,7 +29,7 @@ func Closer[T any](ch chan T, errp *error) { // CloserSend handles panics. // if errp is non-nil, panic values updates it using errors.AppendError. func CloserSend[T any](ch chan<- T, errp *error) { - defer Recover(Annotation(), errp, NoOnError) + defer PanicToErr(errp) close(ch) } @@ -38,7 +38,7 @@ func CloserSend[T any](ch chan<- T, errp *error) { // Close handles panics. // if errp is non-nil, panic values updates it using errors.AppendError. func Close(closable io.Closer, errp *error) { - defer Recover(Annotation(), errp, NoOnError) + defer PanicToErr(errp) if e := closable.Close(); e != nil { *errp = perrors.AppendError(*errp, e) diff --git a/cyclic-awaitable.go b/cyclic-awaitable.go index ba86ee3f..f6451d55 100644 --- a/cyclic-awaitable.go +++ b/cyclic-awaitable.go @@ -8,7 +8,9 @@ package parl import "sync/atomic" const ( - CyclicAwaitableClosed = true + // as argument to NewCyclicAwaitable, causes the awaitable ot be initially + // triggered + CyclicAwaitableClosed bool = true ) // CyclicAwaitable is an awaitable that can be re-initialized @@ -19,67 +21,47 @@ const ( // allows for race-free consumers // - Close is idempotent, panic-free // - if atomic.Pointer[Awaitable] is used for retrieval, a cyclic semaphore is achieved -type CyclicAwaitable atomic.Pointer[Awaitable] +type CyclicAwaitable struct{ *atomic.Pointer[Awaitable] } // NewCyclicAwaitable returns an awaitable that can be re-initialized -// - Init must be invoked prior to use -func NewCyclicAwaitable() (awaitable *CyclicAwaitable) { - return &CyclicAwaitable{} -} - -// Init sets the initial state of the awaitable -// - default is not triggered -// - if argument [task.CyclicAwaitableClosed], initial state -// is triggered -func (a *CyclicAwaitable) Init(initiallyClosed ...bool) (a2 *CyclicAwaitable) { - a2 = a - var shouldBeClosed = len(initiallyClosed) > 0 && initiallyClosed[0] - var awaitable = NewAwaitable() - if shouldBeClosed { - awaitable.Close() +// - if argument [task.CyclicAwaitableClosed] is provided, the initial state +// of the CyclicAwaitable is triggered +func NewCyclicAwaitable(initiallyClosed ...bool) (awaitable *CyclicAwaitable) { + c := CyclicAwaitable{Pointer: &atomic.Pointer[Awaitable]{}} + c.Store(NewAwaitable()) + if len(initiallyClosed) > 0 && initiallyClosed[0] { + c.Close() } - (*atomic.Pointer[Awaitable])(a).Store(awaitable) - return + return &c } // Ch returns an awaitable channel. Thread-safe -func (a *CyclicAwaitable) Ch() (ch AwaitableCh) { - return (*atomic.Pointer[Awaitable])(a).Load().Ch() -} +func (a *CyclicAwaitable) Ch() (ch AwaitableCh) { return a.Pointer.Load().Ch() } // isClosed inspects whether the awaitable has been triggered // - isClosed indicates that the channel is closed -// - isAboutToClose indicates that Close has been invoked, -// but that channel close may still be in progress -// - if isClosed is true, isAboutToClose is also true -// - the two values are requried to attain race-free consumers // - Thread-safe -func (a *CyclicAwaitable) IsClosed() (isClosed, isAboutToClose bool) { - return (*atomic.Pointer[Awaitable])(a).Load().IsClosed() -} +func (a *CyclicAwaitable) IsClosed() (isClosed bool) { return a.Load().IsClosed() } // Close triggers awaitable by closing the channel // - upon return, the channel is guaranteed to be closed // - idempotent, panic-free, thread-safe -func (a *CyclicAwaitable) Close() (didClose bool) { - return (*atomic.Pointer[Awaitable])(a).Load().Close() -} +func (a *CyclicAwaitable) Close() (didClose bool) { return a.Load().Close() } // Open rearms the awaitable for another cycle // - upon return, the channel is guarantee to be open // - idempotent, panic-free, thread-safe func (a *CyclicAwaitable) Open() (didOpen bool) { - var openp *Awaitable + var openedAwaitable *Awaitable for { - var ap = (*atomic.Pointer[Awaitable])(a).Load() - var isClosed, _ = ap.IsClosed() - if !isClosed { + var awaitable = a.Load() + if !awaitable.IsClosed() { return // was open return } - if openp == nil { - openp = NewAwaitable() + if openedAwaitable == nil { + openedAwaitable = NewAwaitable() } - if didOpen = (*atomic.Pointer[Awaitable])(a).CompareAndSwap(ap, openp); didOpen { + if didOpen = a.CompareAndSwap(awaitable, openedAwaitable); didOpen { return // did open the channel return } } diff --git a/debouncer.go b/debouncer.go index 219b7de3..4ddfd7f0 100644 --- a/debouncer.go +++ b/debouncer.go @@ -79,7 +79,7 @@ func (d *Debouncer[T]) Wait() { // debouncerThread debounces the in channel until it closes or context cancel func (d *Debouncer[T]) inputThread() { defer close(d.inputEndCh) - defer Recover(Annotation(), nil, d.errFn) + defer Recover("", nil, d.errFn) // read input channel save in buffer and reset timer var noShutdown = true @@ -114,7 +114,7 @@ func (d *Debouncer[T]) inputThread() { // debouncerThread debounces the in channel until it closes or context cancel func (d *Debouncer[T]) outputThread() { defer close(d.outputEndCh) - defer Recover(Annotation(), nil, d.errFn) + defer Recover("", nil, d.errFn) // wait for timer to elapse or input thread to exit var inputThreadOK = true diff --git a/do-func.go b/do-func.go index ff75876d..f83cfba0 100644 --- a/do-func.go +++ b/do-func.go @@ -10,7 +10,7 @@ package parl func DoThread(op func() (err error), g0 Go) { var err error defer g0.Done(&err) - defer Recover(Annotation(), &err, NoOnError) + defer PanicToErr(&err) err = op() } @@ -18,7 +18,7 @@ func DoThread(op func() (err error), g0 Go) { func DoProcThread(op func(), g0 Go) { var err error defer g0.Done(&err) - defer Recover(Annotation(), &err, NoOnError) + defer PanicToErr(&err) op() } @@ -31,7 +31,7 @@ func DoThreadError(op func() (err error), errCh chan<- error, g0 Go) { errCh <- err err = nil }() - defer Recover(Annotation(), &err, NoOnError) + defer PanicToErr(&err) err = op() } diff --git a/ensure-error.go b/ensure-error.go new file mode 100644 index 00000000..96f374e0 --- /dev/null +++ b/ensure-error.go @@ -0,0 +1,54 @@ +/* +© 2020–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package parl + +import ( + "fmt" + + "github.com/haraldrudell/parl/perrors" +) + +const ( + // counts the stack frame of [parl.ensureError] + parlEnsureErrorFrames = 1 + // counts the stack frame of [parl.EnsureError] + parlEnsureErrorFrames0 = 1 +) + +// ensureError interprets a panic values as an error +// - returned value is either nil or an error value with stack trace +// - the error is ensured to have stack trace +func EnsureError(panicValue any) (err error) { + return ensureError(panicValue, parlEnsureErrorFrames0) +} + +// ensureError interprets a panic values as an error +// - returned value is either nil or an error value with stack trace +// - frames is used to select the stack frame from where the stack trace begins +// - frames 0 is he caller of ensure Error +func ensureError(panicValue any, frames int) (err error) { + + // no panic is no-op + if panicValue == nil { + return // no panic return + } + + // ensure value to be error + var ok bool + if err, ok = panicValue.(error); !ok { + err = fmt.Errorf("non-error value: %T %+[1]v", panicValue) + } + + // ensure stack trace + if !perrors.HasStack(err) { + if frames < 0 { + frames = 0 + } + err = perrors.Stackn(err, frames+parlEnsureErrorFrames) + } + + return +} diff --git a/err-ch-wait.go b/err-ch-wait.go index de0bb16b..c186917f 100644 --- a/err-ch-wait.go +++ b/err-ch-wait.go @@ -22,7 +22,7 @@ import "github.com/haraldrudell/parl/perrors" // func someFunc(errCh chan<- error) { // var err error // defer parl.SendErr(errCh, &err) -// defer parl.Recover(parl.Annotation(), &err, parl.NoOnError) +// defer parl.PanicToErr(errp) func ErrChWait(errCh <-chan error, errp *error) { if errp == nil { panic(perrors.NewPF("errp cannot be nil")) diff --git a/errorglue/indices.go b/errorglue/indices.go index 98f772e8..a0df5f24 100644 --- a/errorglue/indices.go +++ b/errorglue/indices.go @@ -22,9 +22,9 @@ const ( // - stack[panicIndex] is the code line causing the panic func Indices(stack pruntime.StackSlice) (isPanic bool, recoveryIndex, panicIndex int) { found := 0 - length := len(stack) + stackLength := len(stack) pd := panicDetectorOne - for i := 0; i < length; i++ { + for i := 0; i < stackLength; i++ { funcName := stack[i].FuncName if i > 0 && funcName == pd.runtimeDeferInvokerLocation { recoveryIndex = i - 1 @@ -33,10 +33,10 @@ func Indices(stack pruntime.StackSlice) (isPanic bool, recoveryIndex, panicIndex break } } - if i+1 < length && funcName == pd.runtimePanicFunctionLocation { + if i+1 < stackLength && funcName == pd.runtimePanicFunctionLocation { // scan for end of runtime functions - for panicIndex = i + 1; panicIndex+1 < length; panicIndex++ { + for panicIndex = i + 1; panicIndex+1 < stackLength; panicIndex++ { if !strings.HasPrefix(stack[panicIndex].FuncLine(), runtimePrefix) { break // this frame not part of the runtime } @@ -52,6 +52,7 @@ func Indices(stack pruntime.StackSlice) (isPanic bool, recoveryIndex, panicIndex return } +// WhyNotPanic returns a printble string explaining panic-data on err func WhyNotPanic(err error) (s string) { stack := GetInnerMostStack(err) isPanic, recoveryIndex, panicIndex := Indices(stack) diff --git a/errorglue/indices_test.go b/errorglue/indices_test.go new file mode 100644 index 00000000..4de4968f --- /dev/null +++ b/errorglue/indices_test.go @@ -0,0 +1,160 @@ +/* +© 2022–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package errorglue + +import ( + "errors" + "fmt" + "runtime" + "strings" + "testing" + + "github.com/haraldrudell/parl/pruntime" +) + +const ( + indicesShouldDetectPanic = true + indicesShouldNotDetectPanic = false + indicesRuntimePrefix = "runtime." +) + +func TestIndices(t *testing.T) { + newIndicesTest("no panic", noPanic, indicesShouldNotDetectPanic, t).run() + newIndicesTest("panic(1)", panicOne, indicesShouldDetectPanic, t).run() + newIndicesTest("nil pointer dereference", panicNilPointer, indicesShouldDetectPanic, t).run() +} + +// noPanic returns a deferred stack trace generated without a panic +func noPanic() (stack pruntime.StackSlice) { + defer func() { + stack = pruntime.NewStackSlice(0) + }() + return +} + +// panicOne returns a deferred stack trace generated by panic(1) +func panicOne() (stack pruntime.StackSlice) { + var one = 1 + defer func() { + if recoverValue := recover(); recoverValue != one { + panic(fmt.Errorf("bad recover value: %T “%[1]v” exp: %d", + recoverValue, + one, + )) + } + stack = pruntime.NewStackSlice(0) + }() + + panic(one) +} + +// panicFunction recovers a panic using [parl.RecoverErr] +// - panicLine is the exact code line of the panic +// - err is the error value produced by [parl.RecoverErr] +func panicNilPointer() (stack pruntime.StackSlice) { + // runtime.errorString “runtime error: invalid memory address or nil pointer dereference” + // - runtime.errorString implements error + // - only methods are Error() and RuntimeError() + var message = "runtime error: invalid memory address or nil pointer dereference" + defer func() { + var recoverValue = recover() + var isOk bool + if err, ok := recoverValue.(error); ok { + var runtimeError runtime.Error + if errors.As(err, &runtimeError) { + isOk = err.Error() == message + } + } + if !isOk { + panic(fmt.Errorf("bad recover value: %T “%[1]v” exp err message: “%s”", + recoverValue, + message, + )) + } + stack = pruntime.NewStackSlice(0) + }() + + // nil pointer dereference panic + _ = *(*int)(nil) + + return +} + +type indicesArgs struct { + stack pruntime.StackSlice +} + +type indicesTest struct { + name string + args indicesArgs + wantIsPanic bool + wantRecoveryIndex int + wantPanicIndex int + t *testing.T +} + +func newIndicesTest(name string, stackGenerator func() (stack pruntime.StackSlice), isPanic bool, t *testing.T) (t2 *indicesTest) { + var tt = indicesTest{ + name: name, + args: indicesArgs{stackGenerator()}, + wantIsPanic: isPanic, + t: t, + } + t2 = &tt + if !isPanic { + return + } + + // caclulate wantPanicIndex and wantRecoveryIndex + var hasRecovery bool + for i := 0; i < len(tt.args.stack); i++ { + + // is this stack frame in the runtime package? + var isRuntime bool + var cL *pruntime.CodeLocation = &tt.args.stack[i] + isRuntime = strings.HasPrefix(cL.FuncName, indicesRuntimePrefix) + + // recovery is the frame before the first runtime-frame + if !hasRecovery { + if !isRuntime { + continue + } + hasRecovery = true + if i > 0 { + tt.wantRecoveryIndex = i - 1 + } + continue + } + + // panic is the frame after the last runtime-frame + if isRuntime { + continue + } + tt.wantPanicIndex = i + break + } + + return +} + +func (tt *indicesTest) run() { + t := tt.t + t.Run(tt.name, func(t *testing.T) { + gotIsPanic, gotRecoveryIndex, gotPanicIndex := Indices(tt.args.stack) + if gotIsPanic != tt.wantIsPanic { + t.Errorf("Indices() gotIsPanic = %v, want %v", gotIsPanic, tt.wantIsPanic) + } + if gotRecoveryIndex != tt.wantRecoveryIndex { + t.Errorf("Indices() gotRecoveryIndex = %v, want %v", gotRecoveryIndex, tt.wantRecoveryIndex) + } + if gotPanicIndex != tt.wantPanicIndex { + t.Errorf("Indices() gotPanicIndex = %v, want %v", gotPanicIndex, tt.wantPanicIndex) + } + }) + if tt.wantIsPanic { + t.Logf("INPUTSTACK for test: %s stack:%s", tt.name, tt.args.stack) + } +} diff --git a/errorglue/panic-detector.go b/errorglue/panic-detector.go index 51119ad8..9297b3d2 100644 --- a/errorglue/panic-detector.go +++ b/errorglue/panic-detector.go @@ -15,8 +15,9 @@ type panicDetector struct { } // panicDetectorOne is a static value facilitating panic detection for this runtime. -// because panicDetectorOne is instantiated during initialization, it is -// subsequently thread-safe. +// panicDetectorOne is created during package initialization and is +// therefore thread-safe. +// - panicDetectorOne is used by [errorglue.Indices] var panicDetectorOne = func() (pd *panicDetector) { p := panicDetector{ runtimeDeferInvokerLocation: runtimeGopanic, diff --git a/g0/factory.go b/g0/factory.go index cb074244..87d8e1be 100644 --- a/g0/factory.go +++ b/g0/factory.go @@ -15,6 +15,6 @@ var GoGroupFactory parl.GoFactory = &goGroupFactory{} type goGroupFactory struct{} -func (gf *goGroupFactory) NewGoGroup(ctx context.Context, onFirstFatal ...parl.GoFatalCallback) (g1 parl.GoGroup) { +func (f *goGroupFactory) NewGoGroup(ctx context.Context, onFirstFatal ...parl.GoFatalCallback) (goGroup parl.GoGroup) { return NewGoGroup(ctx, onFirstFatal...) } diff --git a/g0/go-chain.go b/g0/go-chain.go index 384ea7f6..6d9575ca 100644 --- a/g0/go-chain.go +++ b/g0/go-chain.go @@ -12,24 +12,24 @@ import ( "github.com/haraldrudell/parl" ) -func GoChain(g0 parl.GoGen) (s string) { +func GoChain(g parl.GoGen) (s string) { for { - var s0 = GoNo(g0) + var s0 = GoNo(g) if s == "" { s = s0 } else { s += "—" + s0 } - if g0 == nil { + if g == nil { return - } else if g0 = Parent(g0); g0 == nil { + } else if g = Parent(g); g == nil { return } } } -func Parent(g0 parl.GoGen) (parent parl.GoGen) { - switch g := g0.(type) { +func Parent(g parl.GoGen) (parent parl.GoGen) { + switch g := g.(type) { case *Go: parent = g.goParent.(parl.GoGen) case *GoGroup: @@ -44,23 +44,23 @@ func ContextID(ctx context.Context) (contextID string) { return fmt.Sprintf("%x", parl.Uintptr(ctx)) } -func GoNo(g0 parl.GoGen) (goNo string) { - switch g := g0.(type) { +func GoNo(g parl.GoGen) (goNo string) { + switch g1 := g.(type) { case *Go: - goNo = "Go" + g.id.String() + ":" + g.GoID().String() + goNo = "Go" + g1.id.String() + ":" + g1.GoID().String() case *GoGroup: - if !g.hasErrorChannel.Load() { + if !g1.hasErrorChannel { goNo = "SubGo" - } else if g.parent != nil { + } else if g1.parent != nil { goNo = "SubGroup" } else { goNo = "GoGroup" } - goNo += g.id.String() + goNo += g1.id.String() case nil: goNo = "nil" default: - goNo = fmt.Sprintf("?type:%T", g0) + goNo = fmt.Sprintf("?type:%T", g) } return } diff --git a/g0/go-context.go b/g0/go-context.go index 50a11bba..32917734 100644 --- a/g0/go-context.go +++ b/g0/go-context.go @@ -20,55 +20,73 @@ const ( g1ccPrepend = "— " ) -// goContext is a promotable private field with Cancel and Context methods only. +// goContext is a promotable private field +// - public methods: Cancel() Context() EntityID() // - goContext is based on parl.NewCancelContext type goContext struct { - goEntityID - c atomic.Pointer[context.Context] - cancelListener func() + goEntityID // EntityID() + wg parl.WaitGroup + // - updatable, therefore must be atomic access + // - initialized in constructor, therefore must be pointer + ctxp *atomic.Pointer[context.Context] + + // cancelListener is fucntion invoked immiately prior to + // parl.InvokeCancel + cancelListener atomic.Pointer[func()] } // newGoContext returns a subordinate context with Cancel and Context methods +// - init must be invoked func newGoContext(ctx context.Context) (gc *goContext) { if ctx == nil { panic(perrors.NewPF("ctx cannot be nil")) } var ctx2 = parl.NewCancelContext(ctx) - c := goContext{ + var ctxp atomic.Pointer[context.Context] + ctxp.Store(&ctx2) + return &goContext{ goEntityID: *newGoEntityID(), + ctxp: &ctxp, } - c.c.Store(&ctx2) - return &c -} - -// AddNotifier adds a stack trace to every Cancel invocation -// -// Usage: -// -// threadGroup := g0.NewGoGroup(ctx) -// threadGroup.(*g0.GoGroup).AddNotifier(func(slice pruntime.StackSlice) { -// parl.D("CANCEL %s %s\n\n\n\n\n", g0.GoChain(threadGroup), slice) -// }) -func (c *goContext) AddNotifier(notifier func(slice pruntime.StackSlice)) { - var ctx = parl.AddNotifier1(*c.c.Load(), notifier) - c.c.Store(&ctx) } // Cancel signals shutdown to all threads of a thread-group. func (c *goContext) Cancel() { - if f := c.cancelListener; f != nil { - f() + if f := c.cancelListener.Load(); f != nil { + (*f)() } // if caller is debug, debug-print cancel action if parl.IsThisDebugN(g1ccSkipFrames) { parl.GetDebug(g1ccSkipFrames)("CancelAndContext.Cancel:\n" + pdebug.NewStack(g1ccSkipFrames).Shorts(g1ccPrepend)) } - parl.InvokeCancel(*c.c.Load()) + parl.InvokeCancel(*c.ctxp.Load()) } // Context returns the context of this cancelAndContext. // - Context is used to detect cancel using the receive channel Context.Done. // - Context cancellation has happened when Context.Err is non-nil. func (c *goContext) Context() (ctx context.Context) { - return *c.c.Load() + return *c.ctxp.Load() +} + +// addNotifier adds a stack trace to every Cancel invocation +// +// Usage: +// +// threadGroup := g0.NewGoGroup(ctx) +// threadGroup.(*g0.GoGroup).addNotifier(func(slice pruntime.StackSlice) { +// parl.D("CANCEL %s %s\n\n\n\n\n", g0.GoChain(threadGroup), slice) +// }) +func (c *goContext) addNotifier(notifier func(slice pruntime.StackSlice)) { + for { + var ctxp0 = c.ctxp.Load() + var ctx = parl.AddNotifier1(*ctxp0, notifier) + if c.ctxp.CompareAndSwap(ctxp0, &ctx) { + return + } + } +} + +func (c *goContext) setCancelListener(f func()) { + c.cancelListener.Store(&f) } diff --git a/g0/go-entity-id.go b/g0/go-entity-id.go index b371da7d..717cbdec 100644 --- a/g0/go-entity-id.go +++ b/g0/go-entity-id.go @@ -6,43 +6,21 @@ ISC License package g0 import ( - "strconv" - "github.com/haraldrudell/parl" ) -// GoEntityID is a unique named type for Go objects -// - GoEntityID is required becaue for Go objects, the thread ID is not available -// prior to the go statement and GoGroups do not have any other unique ID -// - GoEntityID is suitable as a map key -// - GoEntityID uniquely identifies any Go-thread GoGroup, SubGo or SubGroup -type GoEntityID uint64 - -// GoEntityIDs is a generator for Go Object IDs -var GoEntityIDs parl.UniqueIDTypedUint64[GoEntityID] - -// goEntityID provides name, ID and creation time for Go objects +// goEntityID is an unexported unique ID for a parl.Go object // - every Go object has this identifier -// - because every Go object can also be waited upon, goEntityID also has an -// observable wait group -// - only public methods are G0ID() Wait() String() type goEntityID struct { - id GoEntityID - wg parl.WaitGroup // Wait() + id parl.GoEntityID } // newGoEntityID returns a new goEntityID that uniquely identifies a Go object func newGoEntityID() (g0EntityID *goEntityID) { - return &goEntityID{id: GoEntityIDs.ID()} + return &goEntityID{id: parl.GoEntityIDs.ID()} } -// G0ID returns GoEntityID, an internal unique idntifier -func (gi *goEntityID) G0ID() (id GoEntityID) { - return gi.id -} - -func (gi *goEntityID) Wait() { gi.wg.Wait() } - -func (gi GoEntityID) String() (s string) { - return strconv.FormatUint(uint64(gi), 10) +// EntityID returns GoEntityID, an internal unique idntifier +func (i *goEntityID) EntityID() (id parl.GoEntityID) { + return i.id } diff --git a/g0/go-error.go b/g0/go-error.go index 3973c1c8..7e70d905 100644 --- a/g0/go-error.go +++ b/g0/go-error.go @@ -34,44 +34,44 @@ func NewGoError(err error, errContext parl.GoErrorContext, g0 parl.Go) (goError // Error returns a human-readable error message making GoError implement error // - for nil errors, empty string is returned -func (ge *GoError) Error() (message string) { - if ge.err != nil { - message = ge.err.Error() +func (e *GoError) Error() (message string) { + if e.err != nil { + message = e.err.Error() } return } // Time returns when the GoError was created -func (ge *GoError) Time() (when time.Time) { - return ge.t +func (e *GoError) Time() (when time.Time) { + return e.t } // Err returns the unbderlying error -func (ge *GoError) Err() (err error) { - return ge.err +func (e *GoError) Err() (err error) { + return e.err } -func (ge *GoError) IsThreadExit() (isThreadExit bool) { - return ge.errContext == parl.GeExit || - ge.errContext == parl.GePreDoneExit +func (e *GoError) IsThreadExit() (isThreadExit bool) { + return e.errContext == parl.GeExit || + e.errContext == parl.GePreDoneExit } -func (ge *GoError) IsFatal() (isThreadExit bool) { - return (ge.errContext == parl.GeExit || - ge.errContext == parl.GePreDoneExit) && - ge.err != nil +func (e *GoError) IsFatal() (isThreadExit bool) { + return (e.errContext == parl.GeExit || + e.errContext == parl.GePreDoneExit) && + e.err != nil } -func (ge *GoError) ErrContext() (errContext parl.GoErrorContext) { - return ge.errContext +func (e *GoError) ErrContext() (errContext parl.GoErrorContext) { + return e.errContext } -func (ge *GoError) Go() (g0 parl.Go) { - return ge.g0 +func (e *GoError) Go() (g0 parl.Go) { + return e.g0 } -func (ge *GoError) String() (s string) { - err := ge.err +func (e *GoError) String() (s string) { + err := e.err if stack := errorglue.GetInnerMostStack(err); len(stack) > 0 { s = "-at:" + stack[0].Short() } @@ -81,5 +81,5 @@ func (ge *GoError) String() (s string) { } else { message = "OK" } - return "error:\x27" + message + "\x27context:" + ge.errContext.String() + s + return "error:\x27" + message + "\x27context:" + e.errContext.String() + s } diff --git a/g0/go-group.go b/g0/go-group.go index 71e126d9..8a9ee9fb 100644 --- a/g0/go-group.go +++ b/g0/go-group.go @@ -40,31 +40,31 @@ const ( // - new Go threads are handled by the g1WaitGroup // - SubGroup creates a subordinate thread-group using this threadgroup’s error channel type GoGroup struct { - creator pruntime.CodeLocation - parent goGroupParent - hasErrorChannel atomic.Bool // this GoGroup uses its error channel: NewGoGroup() or SubGroup() - isSubGroup atomic.Bool // is SubGroup(): not NewGoGroup() or SubGo() - hadFatal atomic.Bool - onFirstFatal parl.GoFatalCallback - gos parli.ThreadSafeMap[GoEntityID, *ThreadData] - goContext // Cancel() Context() - ch parl.NBChan[parl.GoError] - noTermination atomic.Bool - isDebug atomic.Bool - aggregateThreads atomic.Bool - // doneLock ensures: - // - consistency of data during GoDone - // - change in isWaitGroupDone and g0.goEntityID.wg.DoneBool is atomic - // - order of emitted termination goErrors - // - therefore, doneLock is used in GoDone Add EnableTermination - doneLock sync.Mutex // for GoDone method + creator pruntime.CodeLocation + parent goGroupParent + hasErrorChannel bool // this GoGroup uses its error channel: GoGroup or SubGroup + isSubGroup bool // is SubGroup: not GoGroup or SubGo + onFirstFatal parl.GoFatalCallback + gos parli.ThreadSafeMap[parl.GoEntityID, *ThreadData] + ch parl.NBChan[parl.GoError] // endCh is a channel that closes when this threadGroup ends - // - endCh.Ch() is awaitable channel - // - endCh.IsClosed() indicates ended - endCh parl.ClosableChan[struct{}] + endCh parl.Awaitable + // provides Go entity ID, sub-object waitgroup, cancel-context + *goContext // Cancel() Context() EntityID() - owLock sync.Mutex - onceWaiter *parl.OnceWaiter + hadFatal atomic.Bool + isNoTermination atomic.Bool + isDebug atomic.Bool + isAggregateThreads atomic.Bool + onceWaiter atomic.Pointer[parl.OnceWaiter] + + // doneLock ensures: + // - order of emitted termination goErrors by GoDone + // - atomicity of goContext.Done() endCh.Close() ch.Close() in + // GoDone Cancel EnableTermination + // - ensuring that Add is prevented on GoGroup termination + // - doneLock is used in GoDone Add EnableTermination Cancel + doneLock sync.Mutex // for GoDone method } var _ goGroupParent = &GoGroup{} @@ -93,16 +93,21 @@ func NewGoGroup(ctx context.Context, onFirstFatal ...parl.GoFatalCallback) (g0 p // - Go is invoked by a g0-package consumer // - the Go return value is to be used as a function argument in a go-statement // function-call launching a goroutine thread -func (g0 *GoGroup) Go() (g1 parl.Go) { - return g0.newGo(goGroupStackFrames) +func (g *GoGroup) Go() (g1 parl.Go) { + return g.newGo(goGroupStackFrames) } -func (g0 *GoGroup) FromGoGo() (g1 parl.Go) { - return g0.newGo(goFromGoStackFrames) +// FromGoGo returns a parl.Go thread-features object invoked from another +// parl.Go object +// - the Go return value is to be used as a function argument in a go-statement +// function-call launching a goroutine thread +func (g *GoGroup) FromGoGo() (g1 parl.Go) { + return g.newGo(goFromGoStackFrames) } -func (g0 *GoGroup) newGo(frames int) (g1 parl.Go) { - if g0.isEnd() { +// newGo creates parl.Go objects +func (g *GoGroup) newGo(frames int) (g1 parl.Go) { + if g.isEnd() { panic(perrors.NewPF("after GoGroup termination")) } @@ -113,11 +118,11 @@ func (g0 *GoGroup) newGo(frames int) (g1 parl.Go) { // the only location creating Go objects var threadData *ThreadData - var goEntityID GoEntityID - g1, goEntityID, threadData = newGo(g0, goInvocation) + var goEntityID parl.GoEntityID + g1, goEntityID, threadData = newGo(g, goInvocation) // count the running thread in this thread-group and its parents - g0.Add(goEntityID, threadData) + g.Add(goEntityID, threadData) return } @@ -134,12 +139,13 @@ func (g0 *GoGroup) newGo(frames int) (g1 parl.Go) { // threads invoke the SubGo’s Cancel method // - the SubGo thread-group terminates when all threads in its own thread-group and // that of any subordinate thread-groups have exited. -func (g0 *GoGroup) SubGo(onFirstFatal ...parl.GoFatalCallback) (g1 parl.SubGo) { - return new(g0, nil, false, false, goGroupNewObjectFrames, onFirstFatal...) +func (g *GoGroup) SubGo(onFirstFatal ...parl.GoFatalCallback) (g1 parl.SubGo) { + return new(g, nil, false, false, goGroupNewObjectFrames, onFirstFatal...) } -func (g0 *GoGroup) FromGoSubGo(onFirstFatal ...parl.GoFatalCallback) (g1 parl.SubGo) { - return new(g0, nil, false, false, fromGoNewFrames, onFirstFatal...) +// FromGoSubGo returns a subordinate thread-group witthout an error channel. Thread-safe. +func (g *GoGroup) FromGoSubGo(onFirstFatal ...parl.GoFatalCallback) (g1 parl.SubGo) { + return new(g, nil, false, false, fromGoNewFrames, onFirstFatal...) } // newSubGroup returns a subordinate thread-group with an error channel handling fatal @@ -155,12 +161,14 @@ func (g0 *GoGroup) FromGoSubGo(onFirstFatal ...parl.GoFatalCallback) (g1 parl.Su // threads invoke the SubGroup’s Cancel method // - SubGroup thread-group terminates when its error channel closes after all of its threads // and threads of its subordinate thread-groups have exited. -func (g0 *GoGroup) SubGroup(onFirstFatal ...parl.GoFatalCallback) (g1 parl.SubGroup) { - return new(g0, nil, true, true, goGroupNewObjectFrames, onFirstFatal...) +func (g *GoGroup) SubGroup(onFirstFatal ...parl.GoFatalCallback) (g1 parl.SubGroup) { + return new(g, nil, true, true, goGroupNewObjectFrames, onFirstFatal...) } -func (g0 *GoGroup) FromGoSubGroup(onFirstFatal ...parl.GoFatalCallback) (g1 parl.SubGroup) { - return new(g0, nil, true, true, fromGoNewFrames, onFirstFatal...) +// FromGoSubGroup returns a subordinate thread-group with an error channel handling fatal +// errors only. Thread-safe. +func (g *GoGroup) FromGoSubGroup(onFirstFatal ...parl.GoFatalCallback) (g1 parl.SubGroup) { + return new(g, nil, true, true, fromGoNewFrames, onFirstFatal...) } // new returns a new GoGroup as parl.GoGroup @@ -176,8 +184,9 @@ func new( g := GoGroup{ creator: *pruntime.NewCodeLocation(stackOffset), parent: parent, - goContext: *newGoContext(ctx), - gos: pmaps.NewRWMap[GoEntityID, *ThreadData](), + goContext: newGoContext(ctx), + gos: pmaps.NewRWMap[parl.GoEntityID, *ThreadData](), + endCh: *parl.NewAwaitable(), } if parl.IsThisDebug() { g.isDebug.Store(true) @@ -186,10 +195,10 @@ func new( g.onFirstFatal = onFirstFatal[0] } if hasErrorChannel { - g.hasErrorChannel.Store(true) + g.hasErrorChannel = true } if isSubGroup { - g.isSubGroup.Store(true) + g.isSubGroup = true } if g.isDebug.Load() { s := "new:" + g.typeString() @@ -204,116 +213,118 @@ func new( } // Add processes a thread from this or a subordinate thread-group -func (g0 *GoGroup) Add(goEntityID GoEntityID, threadData *ThreadData) { - g0.doneLock.Lock() - defer g0.doneLock.Unlock() +func (g *GoGroup) Add(goEntityID parl.GoEntityID, threadData *ThreadData) { + g.doneLock.Lock() + defer g.doneLock.Unlock() - g0.wg.Add(1) - if g0.isDebug.Load() { - parl.Log("goGroup#%s:Add(id%s:%s)#%d", g0.G0ID(), goEntityID, threadData.Short(), g0.goEntityID.wg.Count()) + g.wg.Add(1) + if g.isDebug.Load() { + parl.Log("goGroup#%s:Add(id%s:%s)#%d", g.EntityID(), goEntityID, threadData.Short(), g.goContext.wg.Count()) } - if g0.aggregateThreads.Load() { - g0.gos.Put(goEntityID, threadData) + if g.isAggregateThreads.Load() { + g.gos.Put(goEntityID, threadData) } - if g0.parent != nil { - g0.parent.Add(goEntityID, threadData) + if g.parent != nil { + g.parent.Add(goEntityID, threadData) } } -func (g0 *GoGroup) UpdateThread(goEntityID GoEntityID, threadData *ThreadData) { - if g0.aggregateThreads.Load() { - g0.gos.Put(goEntityID, threadData) +// UpdateThread recursively updates thread information for a parl.Go object +// invoked when that Go fiorst obtains the information +func (g *GoGroup) UpdateThread(goEntityID parl.GoEntityID, threadData *ThreadData) { + if g.isAggregateThreads.Load() { + g.gos.Put(goEntityID, threadData) } - if g0.parent != nil { - g0.parent.UpdateThread(goEntityID, threadData) + if g.parent != nil { + g.parent.UpdateThread(goEntityID, threadData) } } // Done receives thread exits from threads in subordinate thread-groups -func (g0 *GoGroup) GoDone(thread parl.Go, err error) { - if g0.endCh.IsClosed() { +func (g *GoGroup) GoDone(thread parl.Go, err error) { + if g.endCh.IsClosed() { panic(perrors.ErrorfPF("in GoGroup after termination: %s", perrors.Short(err))) } // first fatal thread-exit of this thread-group - if err != nil && g0.hadFatal.CompareAndSwap(false, true) { + if err != nil && g.hadFatal.CompareAndSwap(false, true) { // handle FirstFatal() - g0.setFirstFatal() + g.setFirstFatal() // onFirstFatal callback - if g0.onFirstFatal != nil { + if g.onFirstFatal != nil { var errPanic error parl.RecoverInvocationPanic(func() { - g0.onFirstFatal(g0) + g.onFirstFatal(g) }, &errPanic) if errPanic != nil { - g0.ConsumeError(NewGoError( + g.ConsumeError(NewGoError( perrors.ErrorfPF("onFatal callback: %w", errPanic), parl.GeNonFatal, thread)) } } } // atomic operation: DoneBool and g0.ch.Close - g0.doneLock.Lock() - defer g0.doneLock.Unlock() - if g0.endCh.IsClosed() { + g.doneLock.Lock() + defer g.doneLock.Unlock() + + // check inside lock + if g.endCh.IsClosed() { panic(perrors.ErrorfPF("in GoGroup after termination: %s", perrors.Short(err))) } - if g0.isDebug.Load() { + // debug print termination-start + if g.isDebug.Load() { var threadData parl.ThreadData var id string if thread != nil { threadData = thread.ThreadInfo() - id = thread.(*Go).G0ID().String() + id = thread.EntityID().String() } - parl.Log("goGroup#%s:GoDone(%sid%s,%s)after#:%d", g0.G0ID(), threadData.Short(), id, perrors.Short(err), g0.goEntityID.wg.Count()-1) + parl.Log("goGroup#%s:GoDone(%sid%s,%s)after#:%d", g.EntityID(), threadData.Short(), id, perrors.Short(err), g.goContext.wg.Count()-1) } - // process thread-exit - var isTermination = g0.goEntityID.wg.DoneBool() - var goImpl *Go - var ok bool - if goImpl, ok = thread.(*Go); !ok { - panic(perrors.NewPF("type assertion failed")) - } - g0.gos.Delete(goImpl.G0ID(), parli.MapDeleteWithZeroValue) - if g0.isSubGroup.Load() { + // indicates that this GoGroup is about to terminate + // - DoneBool invokes Done and returns status + var isTermination = g.goContext.wg.DoneBool() + + // delete thread from thread-map + g.gos.Delete(thread.EntityID(), parli.MapDeleteWithZeroValue) - // SubGroup with its own error channel with fatals not affecting parent - // send fatal error to parent as non-fatal error with error context GeLocalChan + // SubGroup with its own error channel with fatals not affecting parent + // - send fatal error to parent as non-fatal error with + // error context GeLocalChan + if g.isSubGroup { if err != nil { - g0.ConsumeError(NewGoError(err, parl.GeLocalChan, thread)) + g.ConsumeError(NewGoError(err, parl.GeLocalChan, thread)) } // pretend good thread exit to parent - g0.parent.GoDone(thread, nil) + g.parent.GoDone(thread, nil) } - if g0.hasErrorChannel.Load() { - // emit on local error channel - var context parl.GoErrorContext + // emit on local error channel: GoGroup, SubGroup + if g.hasErrorChannel { + var goErrorContext parl.GoErrorContext if isTermination { - context = parl.GeExit + goErrorContext = parl.GeExit } else { - context = parl.GePreDoneExit - } - g0.ch.Send(NewGoError(err, context, thread)) - if isTermination { - g0.ch.Close() // close local error channel + goErrorContext = parl.GePreDoneExit } + g.ch.Send(NewGoError(err, goErrorContext, thread)) } else { - // SubGo case: all forwarded to parent - g0.parent.GoDone(thread, err) + // SubGo: forward error to parent + g.parent.GoDone(thread, err) } - if g0.isDebug.Load() { - s := "goGroup#" + g0.G0ID().String() + ":" + // debug print termination end + if g.isDebug.Load() { + s := "goGroup#" + g.EntityID().String() + ":" if isTermination { - s += parl.Sprintf("Terminated:isSubGroup:%t:hasEc:%t", g0.isSubGroup.Load(), g0.hasErrorChannel.Load()) + s += parl.Sprintf("Terminated:isSubGroup:%t:hasEc:%t", g.isSubGroup, g.hasErrorChannel) } else { - s += Shorts(g0.Threads()) + s += Shorts(g.Threads()) } parl.Log(s) } @@ -321,16 +332,13 @@ func (g0 *GoGroup) GoDone(thread parl.Go, err error) { if !isTermination { return // GoGroup not yet terminated return } - - // mark GoGroup terminated - g0.endCh.Close() - g0.goContext.Cancel() + g.endGoGroup() } // ConsumeError receives non-fatal errors from a Go thread. // Go.AddError delegates to this method -func (g0 *GoGroup) ConsumeError(goError parl.GoError) { - if g0.ch.DidClose() { +func (g *GoGroup) ConsumeError(goError parl.GoError) { + if g.ch.DidClose() { panic(perrors.ErrorfPF("in GoGroup after termination: %s", goError)) } if goError == nil { @@ -347,89 +355,111 @@ func (g0 *GoGroup) ConsumeError(goError parl.GoError) { // it is a non-fatal error that should be processed // if we have a parent GoGroup, send it there - if g0.parent != nil { - g0.parent.ConsumeError(goError) + if g.parent != nil { + g.parent.ConsumeError(goError) return } // send the error to the channel of this stand-alone G1Group - g0.ch.Send(goError) + g.ch.Send(goError) } -func (g0 *GoGroup) Ch() (ch <-chan parl.GoError) { return g0.ch.Ch() } - -func (g0 *GoGroup) FirstFatal() (firstFatal *parl.OnceWaiterRO) { - g0.owLock.Lock() - defer g0.owLock.Unlock() - - if g0.onceWaiter == nil { - g0.onceWaiter = parl.NewOnceWaiter(context.Background()) - if g0.hadFatal.Load() { - g0.onceWaiter.Cancel() +// Ch returns a channel sending the all fatal termination errors when +// the FailChannel option is present, or only the first when both +// FailChannel and StoreSubsequentFail options are present. +func (g *GoGroup) Ch() (ch <-chan parl.GoError) { return g.ch.Ch() } + +// FirstFatal allows to await or inspect the first thread terminating with error. +// it is valid if this SubGo has LocalSubGo or LocalChannel options. +// To wait for first fatal error using multiple-semaphore mechanic: +// +// firstFatal := g0.FirstFatal() +// for { +// select { +// case <-firstFatal.Ch(): +// … +// +// To inspect first fatal: +// +// if firstFatal.DidOccur() … +func (g *GoGroup) FirstFatal() (firstFatal *parl.OnceWaiterRO) { + var onceWaiter *parl.OnceWaiter + for { + if onceWaiter0 := g.onceWaiter.Load(); onceWaiter0 != nil { + return parl.NewOnceWaiterRO(onceWaiter0) + } + if onceWaiter == nil { + onceWaiter = parl.NewOnceWaiter(context.Background()) + } + if g.onceWaiter.CompareAndSwap(nil, onceWaiter) { + onceWaiter.Cancel() + return } } - return parl.NewOnceWaiterRO(g0.onceWaiter) } -func (g0 *GoGroup) EnableTermination(allowTermination bool) { - if g0.isDebug.Load() { - parl.Log("goGroup%s#:EnableTermination:%t", g0.G0ID(), allowTermination) +// EnableTermination false prevents the SubGo or GoGroup from terminating +// even if the number of threads is zero +func (g *GoGroup) EnableTermination(allowTermination bool) { + if g.isDebug.Load() { + parl.Log("goGroup%s#:EnableTermination:%t", g.EntityID(), allowTermination) } - if g0.endCh.IsClosed() { + if g.endCh.IsClosed() { return // GoGroup is already shutdown return } else if !allowTermination { - if g0.noTermination.CompareAndSwap(false, true) { // prevent termination, it was previously allowed - g0.CascadeEnableTermination(1) + if g.isNoTermination.CompareAndSwap(false, true) { // prevent termination, it was previously allowed + // add a fake count to parent waitgroup preventing iut from terminating + g.CascadeEnableTermination(1) } return // prevent termination complete } // now allow termination - if !g0.noTermination.CompareAndSwap(true, false) { + if !g.isNoTermination.CompareAndSwap(true, false) { return // termination allowed already } + // remove the fake count from parent + g.CascadeEnableTermination(-1) // atomic operation: DoneBool and g0.ch.Close - g0.doneLock.Lock() - defer g0.doneLock.Unlock() - - g0.CascadeEnableTermination(-1) - if !g0.wg.IsZero() { - return // GoGroup did not terminate - } + g.doneLock.Lock() + defer g.doneLock.Unlock() - if g0.hasErrorChannel.Load() { - g0.ch.Close() // close local error channel + // if there are suboirdinate objects, termination wwill be done by GoDone + if !g.wg.IsZero() { + return // GoGroup is not in pending termination } - // mark GoGroup terminated - g0.endCh.Close() - g0.goContext.Cancel() + g.endGoGroup() } -func (g0 *GoGroup) IsEnableTermination() (mayTerminate bool) { return !g0.noTermination.Load() } +// IsEnableTermination returns the state of EnableTermination, +// initially true +func (g *GoGroup) IsEnableTermination() (mayTerminate bool) { return !g.isNoTermination.Load() } // CascadeEnableTermination manipulates wait groups of this goGroup and // those of its parents to allow or prevent termination -func (g0 *GoGroup) CascadeEnableTermination(delta int) { - g0.wg.Add(delta) - if g0.parent != nil { - g0.parent.CascadeEnableTermination(delta) +func (g *GoGroup) CascadeEnableTermination(delta int) { + g.wg.Add(delta) + if g.parent != nil { + g.parent.CascadeEnableTermination(delta) } } -func (g0 *GoGroup) ThreadsInternal() (orderedMap pmaps.KeyOrderedMap[GoEntityID, parl.ThreadData]) { - orderedMap = *pmaps.NewKeyOrderedMap[GoEntityID, parl.ThreadData]() - var rwm = g0.gos.(*pmaps.RWMap[GoEntityID, *ThreadData]) - rwm.Range(func(key GoEntityID, value *ThreadData) (kepGoing bool) { +// the available data for all threads as a map +func (g *GoGroup) ThreadsInternal() (orderedMap pmaps.KeyOrderedMap[parl.GoEntityID, parl.ThreadData]) { + orderedMap = *pmaps.NewKeyOrderedMap[parl.GoEntityID, parl.ThreadData]() + var rwm = g.gos.(*pmaps.RWMap[parl.GoEntityID, *ThreadData]) + rwm.Range(func(key parl.GoEntityID, value *ThreadData) (kepGoing bool) { orderedMap.Put(key, value) return true }) return } -func (g0 *GoGroup) Threads() (threads []parl.ThreadData) { +// the available data for all threads +func (g *GoGroup) Threads() (threads []parl.ThreadData) { // the pointer can be updated at any time, but the value does not change - list := g0.gos.List() + list := g.gos.List() threads = make([]parl.ThreadData, len(list)) for i, tp := range list { threads[i] = tp @@ -437,9 +467,10 @@ func (g0 *GoGroup) Threads() (threads []parl.ThreadData) { return } -func (g0 *GoGroup) NamedThreads() (threads []parl.ThreadData) { +// threads that have been named ordered by name +func (g *GoGroup) NamedThreads() (threads []parl.ThreadData) { // the pointer can be updated at any time, but the value does not change - list := g0.gos.List() + list := g.gos.List() // remove unnamed threads for i := 0; i < len(list); { @@ -451,7 +482,7 @@ func (g0 *GoGroup) NamedThreads() (threads []parl.ThreadData) { } // sort pointers - slices.SortFunc(list, g0.cmpNames) + slices.SortFunc(list, g.cmpNames) // return slice of values threads = make([]parl.ThreadData, len(list)) @@ -461,26 +492,37 @@ func (g0 *GoGroup) NamedThreads() (threads []parl.ThreadData) { return } -func (g0 *GoGroup) SetDebug(debug parl.GoDebug) { +// SetDebug enables debug logging on this particular instance +// - parl.NoDebug +// - parl.DebugPrint +// - parl.AggregateThread +func (g *GoGroup) SetDebug(debug parl.GoDebug) { if debug == parl.DebugPrint { - g0.isDebug.Store(true) - g0.aggregateThreads.Store(true) + g.isDebug.Store(true) + g.isAggregateThreads.Store(true) return } - g0.isDebug.Store(false) + g.isDebug.Store(false) if debug == parl.AggregateThread { - g0.aggregateThreads.Store(true) + g.isAggregateThreads.Store(true) return } - g0.aggregateThreads.Store(false) + g.isAggregateThreads.Store(false) } // Cancel signals shutdown to all threads of a thread-group. func (g *GoGroup) Cancel() { + + // cancel the context g.goContext.Cancel() - if g.isEnd() || g.goContext.wg.Count() > 0 || g.noTermination.Load() { + + // check outside lock: done if: + // - if GoGroup/SubGroup/SubGo already terminated + // - subordinate objects exist + // - termination is temporarily disabled + if g.isEnd() || g.goContext.wg.Count() > 0 || g.isNoTermination.Load() { return // already ended or have child object or termination off return } @@ -490,25 +532,36 @@ func (g *GoGroup) Cancel() { g.doneLock.Lock() defer g.doneLock.Unlock() - if g.isEnd() || g.goContext.wg.Count() > 0 || g.noTermination.Load() { + // repeat check inside lock + if g.isEnd() || g.goContext.wg.Count() > 0 || g.isNoTermination.Load() { return // already ended or have child object or termination off return } - if g.hasErrorChannel.Load() { - g.ch.Close() // close local error channel - } - // mark GoGroup terminated - g.endCh.Close() + g.endGoGroup() } -func (g0 *GoGroup) Wait() { - <-g0.endCh.Ch() +// Wait waits for all threads of this thread-group to terminate. +func (g *GoGroup) Wait() { + <-g.endCh.Ch() } -func (g0 *GoGroup) WaitCh() (ch <-chan struct{}) { - return g0.endCh.Ch() +// returns a channel that closes on subGo end similar to Wait +func (g *GoGroup) WaitCh() (ch parl.AwaitableCh) { + return g.endCh.Ch() } -func (g0 *GoGroup) cmpNames(a *ThreadData, b *ThreadData) (result int) { +// invoked while holding g.doneLock +func (g *GoGroup) endGoGroup() { + if g.hasErrorChannel { + g.ch.Close() // close local error channel + } + // mark GoGroup terminated + g.endCh.Close() + // cancel the context + g.goContext.Cancel() +} + +// cmpNames is a slice comparison function for thread names +func (g *GoGroup) cmpNames(a *ThreadData, b *ThreadData) (result int) { if a.label < b.label { return -1 } else if a.label > b.label { @@ -517,15 +570,13 @@ func (g0 *GoGroup) cmpNames(a *ThreadData, b *ThreadData) (result int) { return 0 } -func (g0 *GoGroup) setFirstFatal() { - g0.owLock.Lock() - defer g0.owLock.Unlock() - - if g0.onceWaiter == nil { +// setFirstFatal triggers a possible onFirstFatal +func (g *GoGroup) setFirstFatal() { + var onceWaiter = g.onceWaiter.Load() + if onceWaiter == nil { return // FirstFatal not invoked return } - - g0.onceWaiter.Cancel() + onceWaiter.Cancel() } // isEnd determines if this goGroup has ended @@ -535,34 +586,41 @@ func (g0 *GoGroup) setFirstFatal() { // - — a subGo, having no error channel, ends when all threads have exited // - if the GoGroup or any of its subordinate thread-groups have EnableTermination false // GoGroups will not end until EnableTermination true -func (g0 *GoGroup) isEnd() (isEnd bool) { +func (g *GoGroup) isEnd() (isEnd bool) { // SubGo termination flag - if !g0.hasErrorChannel.Load() { - return g0.endCh.IsClosed() + if !g.hasErrorChannel { + return g.endCh.IsClosed() } - // others is by error channel — wait until all errors have been read - return g0.ch.IsClosed() + // others is when error channel closes + var ch = g.ch.Ch() + select { + case <-ch: + isEnd = true + default: + } + + return } // "goGroup#1" "subGroup#2" "subGo#3" -func (g0 *GoGroup) typeString() (s string) { - if g0.parent == nil { +func (g *GoGroup) typeString() (s string) { + if g.parent == nil { s = "goGroup" - } else if g0.isSubGroup.Load() { + } else if g.isSubGroup { s = "subGroup" } else { s = "subGo" } - return s + "#" + g0.goEntityID.G0ID().String() + return s + "#" + g.goEntityID.EntityID().String() } // g1Group#3threads:1(1)g0.TestNewG1Group-g1-group_test.go:60 -func (g0 *GoGroup) String() (s string) { +func (g *GoGroup) String() (s string) { return parl.Sprintf("%s_threads:%s_New:%s", - g0.typeString(), // "goGroup#1" - g0.goEntityID.wg.String(), - g0.creator.Short(), + g.typeString(), // "goGroup#1" + g.goContext.wg.String(), + g.creator.Short(), ) } diff --git a/g0/go-group_test.go b/g0/go-group_test.go index 8ad273ab..59743543 100644 --- a/g0/go-group_test.go +++ b/g0/go-group_test.go @@ -137,18 +137,18 @@ func TestGoGroup(t *testing.T) { goGroup = NewGoGroup(context.Background()) subGo = goGroup.SubGo() goGroupImpl = subGo.(*GoGroup) - if goGroupImpl.isSubGroup.Load() { + if goGroupImpl.isSubGroup { t.Error("SubGo returned SubGroup") } - if goGroupImpl.hasErrorChannel.Load() { + if goGroupImpl.hasErrorChannel { t.Error("SubGo has error channel") } subGroup = goGroup.SubGroup() goGroupImpl = subGroup.(*GoGroup) - if !goGroupImpl.isSubGroup.Load() { + if !goGroupImpl.isSubGroup { t.Error("SubGroup did not return SubGroup") } - if !goGroupImpl.hasErrorChannel.Load() { + if !goGroupImpl.hasErrorChannel { t.Error("SubGroup does not have error channel") } @@ -171,8 +171,9 @@ func TestGoGroup(t *testing.T) { // Add() UpdateThread() SetDebug() Threads() NamedThreads() G0ID() goGroup = NewGoGroup(context.Background()) goGroupImpl = goGroup.(*GoGroup) + expectG0ID = uint64(goGroupImpl.goEntityID.id) - if expectG0ID != uint64(goGroupImpl.G0ID()) { + if expectG0ID != uint64(goGroupImpl.EntityID()) { t.Error("goGroupImpl.G0ID bad") } goGroup.Go().Register() @@ -365,11 +366,11 @@ func TestCancel(t *testing.T) { }) var threadGroup = NewGoGroup(ctx) - threadGroup.(*GoGroup).AddNotifier(func(slice pruntime.StackSlice) { + threadGroup.(*GoGroup).addNotifier(func(slice pruntime.StackSlice) { t.Logf("CANCEL %s %s", GoChain(threadGroup), slice) }) var subGroup = threadGroup.SubGroup() - subGroup.(*GoGroup).AddNotifier(func(slice pruntime.StackSlice) { + subGroup.(*GoGroup).addNotifier(func(slice pruntime.StackSlice) { t.Logf("CANCEL %s %s", GoChain(subGroup), slice) }) t.Logf("STATE0: %t %t", threadGroup.Context().Err() != nil, subGroup.Context().Err() != nil) diff --git a/g0/go.go b/g0/go.go index 57e59726..41594660 100644 --- a/g0/go.go +++ b/g0/go.go @@ -14,8 +14,8 @@ import ( ) const ( - // 1 is Go.Register/Go/SubGo/SubGroup/AddError/Done - // 1 is checkState + // counts public method: Go.Register/Go/SubGo/SubGroup/AddError/Done + // and [Go.checkState] grCheckThreadFrames = 2 ) @@ -30,128 +30,135 @@ const ( // - SubGroup creates a subordinate thread-group with its own error channel. // Fatal-error thread-exits in SubGroup can be recovered locally in that thread-group type Go struct { - goEntityID - goParent // Cancel() Context() - creatorThreadId parl.ThreadID - thread ThreadSafeThreadData - // endCh is a channel that closes when this threadGroup ends - // - endCh.Ch() is awaitable channel - // - endCh.IsClosed() indicates ended - endCh parl.ClosableChan[struct{}] + goEntityID // EntityID(), uniquely identifies Go object + goParent // Cancel() Context() + creatorThreadId parl.ThreadID // the thread ID of the goroutine creating this thread + // tis thread’s Thread ID, creator location, go-function and + // possible printable thread-name + thread *ThreadSafeThreadData + endCh parl.Awaitable // channel that closes when this Go ends } -// newGo returns a Go object for a thread operating in a Go thread-group. Thread-safe. +// newGo returns a Go object providing functions to a thread operating in a +// Go thread-group. Thread-safe +// - parent is a GoGroup type configured as GoGroup, SubGo or SubGroup +// - goInvocation is the invoker of Go, ie. the parent thread +// - returns Go entity ID and thread data since the parent will +// immediately need those func newGo(parent goParent, goInvocation *pruntime.CodeLocation) ( g0 parl.Go, - goEntityID GoEntityID, + goEntityID parl.GoEntityID, threadData *ThreadData) { if parent == nil { panic(perrors.NewPF("parent cannot be nil")) } g := Go{ - goEntityID: *newGoEntityID(), - goParent: parent, + goEntityID: *newGoEntityID(), + goParent: parent, + endCh: *parl.NewAwaitable(), + creatorThreadId: goid.GoID(), + thread: NewThreadSafeThreadData(), } - g.wg.Add(1) - g.creatorThreadId = goid.GoID() g.thread.SetCreator(goInvocation) + // return values g0 = &g - goEntityID = g.G0ID() + goEntityID = g.EntityID() threadData = g.thread.Get() + return } -func (g0 *Go) Register(label ...string) (g00 parl.Go) { return g0.checkState(false, label...) } -func (g0 *Go) Go() (g00 parl.Go) { return g0.checkState(false).goParent.FromGoGo() } -func (g0 *Go) SubGo(onFirstFatal ...parl.GoFatalCallback) (subGo parl.SubGo) { - return g0.checkState(false).goParent.FromGoSubGo(onFirstFatal...) +func (g *Go) Register(label ...string) (g00 parl.Go) { return g.ensureThreadData(label...) } +func (g *Go) Go() (g00 parl.Go) { return g.ensureThreadData().goParent.FromGoGo() } +func (g *Go) SubGo(onFirstFatal ...parl.GoFatalCallback) (subGo parl.SubGo) { + return g.ensureThreadData().goParent.FromGoSubGo(onFirstFatal...) } -func (g0 *Go) SubGroup(onFirstFatal ...parl.GoFatalCallback) (subGroup parl.SubGroup) { - return g0.checkState(false).goParent.FromGoSubGroup(onFirstFatal...) +func (g *Go) SubGroup(onFirstFatal ...parl.GoFatalCallback) (subGroup parl.SubGroup) { + return g.ensureThreadData().goParent.FromGoSubGroup(onFirstFatal...) } -func (g0 *Go) AddError(err error) { - g0.checkState(false) +// AddError emits a non-fatal errors +func (g *Go) AddError(err error) { + g.ensureThreadData() if err == nil { return // nil error return } - g0.ConsumeError(NewGoError(perrors.Stack(err), parl.GeNonFatal, g0)) + g.ConsumeError(NewGoError(perrors.Stack(err), parl.GeNonFatal, g)) } // Done handles thread exit. Deferrable -func (g0 *Go) Done(errp *error) { - g0.checkState(true) - var didClose, _ = g0.endCh.Close() - if !didClose { +// - *errp contains possible fatalk thread error +// - errp can be nil +func (g *Go) Done(errp *error) { + if !g.ensureThreadData().endCh.Close() { panic(perrors.ErrorfPF("Go received multiple Done: ", perrors.ErrpString(errp))) } - // obtain error and ensure it has stack + // obtain fatal error and ensure it has stack var err error if errp != nil { err = perrors.Stack(*errp) } - g0.goParent.GoDone(g0, err) - g0.wg.Done() + // notify parent of exit + g.goParent.GoDone(g, err) } -func (g0 *Go) ThreadInfo() (threadData parl.ThreadData) { return g0.thread.Get() } -func (g0 *Go) GoID() (threadID parl.ThreadID) { return g0.thread.ThreadID() } -func (g0 *Go) Creator() (threadID parl.ThreadID, createLocation *pruntime.CodeLocation) { - threadID = g0.creatorThreadId - var threadData = g0.thread.Get() +func (g *Go) ThreadInfo() (threadData parl.ThreadData) { return g.thread.Get() } +func (g *Go) GoID() (threadID parl.ThreadID) { return g.thread.ThreadID() } +func (g *Go) Creator() (threadID parl.ThreadID, createLocation *pruntime.CodeLocation) { + threadID = g.creatorThreadId + var threadData = g.thread.Get() createLocation = &threadData.createLocation return } -func (g0 *Go) GoRoutine() (threadID parl.ThreadID, goFunction *pruntime.CodeLocation) { - var threadData = g0.thread.Get() +func (g *Go) GoRoutine() (threadID parl.ThreadID, goFunction *pruntime.CodeLocation) { + var threadData = g.thread.Get() threadID = threadData.threadID goFunction = &threadData.funcLocation return } - -func (g0 *Go) Wait() { - <-g0.endCh.Ch() -} - -// checkState is invoked by public methods ensuring that terminated -// objects are not being used -// - checkState also collects data on the new thread -func (g0 *Go) checkState(skipTerminated bool, label ...string) (g *Go) { - g = g0 - if !skipTerminated && g0.endCh.IsClosed() { - panic(perrors.NewPF("operation on terminated Go thread object")) +func (g *Go) Wait() { <-g.endCh.Ch() } +func (g *Go) WaitCh() (ch parl.AwaitableCh) { return g.endCh.Ch() } + +// ensureThreadData is invoked by Go’s public methods ensuring that +// the thread’s information is collected +// - label is an optional printable thread-name +// - ensureThreadData supports functional chaining +func (g *Go) ensureThreadData(label ...string) (g1 *Go) { + g1 = g + + // if thread-data has already been collected, do nothing + if g.thread.HaveThreadID() { + return // already have thread-data return } - // ensure we have a threadID - if g0.thread.HaveThreadID() { - return - } - - // update thread information + // optional printable thread name var label0 string if len(label) > 0 { label0 = label[0] } + + // get stack that contains thread ID, go function, go-function invoker + // for the new thread var stack = pdebug.NewStack(grCheckThreadFrames) if stack.IsMain() { return // this should not happen, called by Main } // creator has already been set - g0.thread.Update(stack.ID(), nil, stack.GoFunction(), label0) + g.thread.Update(stack.ID(), nil, stack.GoFunction(), label0) + + // propagate thread information to parent + g.UpdateThread(g.EntityID(), g.thread.Get()) - // propagate thread information - threadData := g0.thread.Get() - g0.UpdateThread(g0.G0ID(), threadData) return } // g1ID:4:g0.(*g1WaitGroup).Go-g1-thread-group.go:63 -func (g0 *Go) String() (s string) { - td := g0.thread.Get() +func (g *Go) String() (s string) { + td := g.thread.Get() return parl.Sprintf("go:%s:%s", td.threadID, td.createLocation.Short()) } diff --git a/g0/go_test.go b/g0/go_test.go index bca7e87b..3d2df5df 100644 --- a/g0/go_test.go +++ b/g0/go_test.go @@ -64,10 +64,6 @@ func TestGo(t *testing.T) { if _, ok = <-goGroup.Ch(); ok { t.Error("g0.Done goGroup errch did not close") } - if !g0Impl.wg.IsZero() { - t.Error("!g0Impl.wg.IsZero") - t.FailNow() - } g0.Wait() // Cancel() Context() diff --git a/g0/if-g0.go b/g0/if-g0.go index e1d62ef0..546662e4 100644 --- a/g0/if-g0.go +++ b/g0/if-g0.go @@ -12,11 +12,11 @@ import ( ) type goGroupParent interface { - Add(id GoEntityID, threadData *ThreadData) + Add(id parl.GoEntityID, threadData *ThreadData) CascadeEnableTermination(delta int) ConsumeError(goError parl.GoError) GoDone(g0 parl.Go, err error) - UpdateThread(goEntityID GoEntityID, threadData *ThreadData) + UpdateThread(goEntityID parl.GoEntityID, threadData *ThreadData) Context() (ctx context.Context) } @@ -27,7 +27,7 @@ type goParent interface { FromGoSubGo(onFirstFatal ...parl.GoFatalCallback) (g0 parl.SubGo) FromGoSubGroup(onFirstFatal ...parl.GoFatalCallback) (g0 parl.SubGroup) GoDone(g0 parl.Go, err error) - UpdateThread(goEntityID GoEntityID, threadData *ThreadData) + UpdateThread(goEntityID parl.GoEntityID, threadData *ThreadData) Cancel() Context() (ctx context.Context) } diff --git a/g0/thread-logger.go b/g0/thread-logger.go index 18955a22..9db44de0 100644 --- a/g0/thread-logger.go +++ b/g0/thread-logger.go @@ -82,10 +82,10 @@ func (t *ThreadLogger) Log() (t2 *ThreadLogger) { close(t.endCh) return // thread-group already ended } - g.aggregateThreads.Store(true) + g.isAggregateThreads.Store(true) if g.Context().Err() == nil { - g.goContext.cancelListener = t.cancelListener + g.goContext.setCancelListener(t.cancelListener) log(threadLoggerLabel + ": listening for Cancel") return } @@ -117,7 +117,7 @@ func (t *ThreadLogger) printThread() { var g = t.goGroup var log = t.log defer close(t.endCh) - defer parl.Recover(parl.Annotation(), nil, parl.Infallible) + defer parl.Recover("", nil, parl.Infallible) defer func() { log("%s %s: %s", parl.ShortSpace(), threadLoggerLabel, "thread-group ended") }() // ticker for periodic printing @@ -125,7 +125,7 @@ func (t *ThreadLogger) printThread() { defer ticker.Stop() var endCh <-chan struct{} - if g.hasErrorChannel.Load() { + if g.hasErrorChannel { endCh = g.ch.WaitForCloseCh() } else { endCh = g.endCh.Ch() diff --git a/g0/thread-safe-thread-data.go b/g0/thread-safe-thread-data.go index 1a8bce54..86a7d653 100644 --- a/g0/thread-safe-thread-data.go +++ b/g0/thread-safe-thread-data.go @@ -23,6 +23,10 @@ type ThreadSafeThreadData struct { td ThreadData } +func NewThreadSafeThreadData() (t *ThreadSafeThreadData) { + return &ThreadSafeThreadData{} +} + // HaveThreadID indicates whether Update has been invoked on this ThreadDataWrap // object. func (tw *ThreadSafeThreadData) HaveThreadID() (haveThreadID bool) { diff --git a/go-entity-id.go b/go-entity-id.go new file mode 100644 index 00000000..71aefac7 --- /dev/null +++ b/go-entity-id.go @@ -0,0 +1,22 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package parl + +import "strconv" + +// GoEntityID is a unique named type for Go objects +// - GoEntityID is required becaue for Go objects, the thread ID is not available +// prior to the go statement and GoGroups do not have any other unique ID +// - GoEntityID is suitable as a map key +// - GoEntityID uniquely identifies any Go-thread GoGroup, SubGo or SubGroup +type GoEntityID uint64 + +// GoEntityIDs is a generator for Go Object IDs +var GoEntityIDs UniqueIDTypedUint64[GoEntityID] + +func (i GoEntityID) String() (s string) { + return strconv.FormatUint(uint64(i), 10) +} diff --git a/go.mod b/go.mod index 441b3155..49320d1c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.21.3 require ( github.com/google/btree v1.1.2 - github.com/haraldrudell/parl/yamler v0.4.115 + github.com/haraldrudell/parl/yamler v0.4.116 golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/sys v0.13.0 golang.org/x/text v0.13.0 diff --git a/go.sum b/go.sum index fc7b0eba..5054b97f 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/haraldrudell/parl/yamler v0.4.115 h1:D0BUn2pkON3k2yLm0yBC/EE/1cirtZtVUcDwkcKDPgQ= -github.com/haraldrudell/parl/yamler v0.4.115/go.mod h1:N130FM4CNuyAwuPDT0YvF+zlF3tvCRPYtvhQOEExAyc= +github.com/haraldrudell/parl/yamler v0.4.116 h1:bi45DW6izbhntjC93ZQyaNERPiu2SwpkAh4cCyQJDkw= +github.com/haraldrudell/parl/yamler v0.4.116/go.mod h1:2VF8uKY0EV6eTDWQXAowD7liuG/zt8szyoC3eS31NsI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= diff --git a/halt/halt-detector.go b/halt/halt-detector.go index 565da060..120535a3 100644 --- a/halt/halt-detector.go +++ b/halt/halt-detector.go @@ -49,7 +49,7 @@ func NewHaltDetector(reportingThreshold ...time.Duration) (haltDetector *HaltDet func (h *HaltDetector) Thread(g0 parl.Go) { var err error defer g0.Register().Done(&err) - defer parl.Recover(parl.Annotation(), &err, parl.NoOnError) + defer parl.PanicToErr(&err) timeTicker := time.NewTicker(time.Millisecond) defer timeTicker.Stop() diff --git a/if-go.go b/if-go.go index 8c761ce3..c6de8bbc 100644 --- a/if-go.go +++ b/if-go.go @@ -31,7 +31,7 @@ type Go interface { // information on the new thread. // - label is an optional name that can be assigned to a Go goroutine thread Register(label ...string) (g0 Go) - // AddError emits a non-fatal error. + // AddError emits a non-fatal errors AddError(err error) // Go returns a Go object to be provided as a go-statement function-argument // in a function call invocation launching a new gorotuine thread. @@ -59,29 +59,42 @@ type Go interface { // - the SubGroup thread-group terminates when both its own threads have exited and // - the threads of its subordinate thread-groups. SubGroup(onFirstFatal ...GoFatalCallback) (subGroup SubGroup) - // Done indicates that this goroutine has finished. + // Done indicates that this goroutine is exiting // - err == nil means successful exit - // - non-nil err indicates a fatal error. - // - Done is deferrable. + // - non-nil err indicates fatal error + // - deferrable Done(errp *error) // Wait awaits exit of this Go thread. Wait() + WaitCh() (ch AwaitableCh) // Cancel signals for the threads in this Go thread’s parent GoGroup thread-group // and any subordinate thread-groups to exit. Cancel() // Context will Cancel when the parent thread-group Cancels // or Cancel is invoked on this Go object. - // Subordinate thread-groups do not Cancel the context of the Go thread. + // - Subordinate thread-groups do not Cancel the context of the Go thread. Context() (ctx context.Context) // ThreadInfo returns thread data that is partially or fully populated + // - ThreadID may be invalid: threadID.IsValid. + // - goFunction may be zero-value: goFunction.IsSet + // - those values present after public methods of parl.Go has been invoked by + // the new goroutine ThreadInfo() (threadData ThreadData) // values always present Creator() (threadID ThreadID, createLocation *pruntime.CodeLocation) - // ThreadID may be invalid: threadID.IsValid. - // goFunction may be zero-value: goFunction.IsSet + // - ThreadID may be invalid: threadID.IsValid. + // - goFunction may be zero-value: goFunction.IsSet + // - those values present after public methods of parl.Go has been invoked by + // the new goroutine GoRoutine() (threadID ThreadID, goFunction *pruntime.CodeLocation) - // GoID efficiently returns the goroutine ID that mey be invalid + // GoID efficiently returns the goroutine ID that may be invalid + // - valid after public methods of parl.Go has been invoked by + // the new goroutine GoID() (threadID ThreadID) + // EntityID returns a value unique for this Go + // - ordered: usable as map key or for sorting + // - always valid, has .String method + EntityID() (goEntityID GoEntityID) fmt.Stringer } @@ -159,6 +172,8 @@ type GoGroup interface { // EnableTermination false prevents the SubGo or GoGroup from terminating // even if the number of threads is zero EnableTermination(allowTermination bool) + // IsEnableTermination returns the state of EnableTermination, + // initially true IsEnableTermination() (mayTerminate bool) // Cancel terminates the threads in this and subordinate thread-groups. Cancel() @@ -197,7 +212,7 @@ type SubGo interface { // Wait waits for all threads of this thread-group to terminate. Wait() // returns a channel that closes on subGo end similar to Wait - WaitCh() (ch <-chan struct{}) + WaitCh() (ch AwaitableCh) // EnableTermination false prevents the SubGo or GoGroup from terminating // even if the number of threads is zero EnableTermination(allowTermination bool) diff --git a/if-pq.go b/if-pq.go new file mode 100644 index 00000000..96b85580 --- /dev/null +++ b/if-pq.go @@ -0,0 +1,87 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package parl + +import "golang.org/x/exp/constraints" + +// AggregatingPriorityQueue uses cached priority obtained from +// Aggregators that operates on the values outside of the AggregatingPriorityQueue. +// - the Update method reprioritizes an updated value element +type AggregatingPriorityQueue[V any, P constraints.Ordered] interface { + // Get retrieves a possible value container associated with valuep + Get(valuep *V) (aggregator Aggregator[V, P], ok bool) + // Put stores a new value container associated with valuep + // - the valuep is asusmed to not have a node in the queue + Put(valuep *V, aggregator Aggregator[V, P]) + // Update re-prioritizes a value + Update(valuep *V) + // Clear empties the priority queue. The hashmap is left intact. + Clear() + // List returns the first n or default all values by pirority + List(n ...int) (aggregatorQueue []AggregatePriority[V, P]) +} + +// PriorityQueue is a pointer-identity-to-value map of updatable values traversable by rank. +// - PriorityQueue operates directly on value by caching priority from the pritority function. +// - the AddOrUpdate method reprioritizes an updated value element +// - V is a value reference composite type that is comparable, ie. not slice map function. +// Preferrably, V is interface or pointer to struct type. +// - P is an ordered type such as Integer Floating-Point or string, used to rank the V values +// - values are added or updated using AddOrUpdate method distinguished by +// (computer science) identity +// - if the same comparable value V is added again, that value is re-ranked +// - priority P is computed from a value V using the priorityFunc function. +// The piority function may be examining field values of a struct +// - values can have the same rank. If they do, equal rank is provided in insertion order +// - pqs.NewPriorityQueue[V any, P constraints.Ordered] +// - pqs.NewRankingThreadSafe[V comparable, R constraints.Ordered]( +// ranker func(value *V) (rank R))) +type PriorityQueue[V any, P constraints.Ordered] interface { + // AddOrUpdate adds a new value to the prioirty queue or updates the priority of a value + // that has changed. + AddOrUpdate(value *V) + // List returns the first n or default all values by priority + List(n ...int) (valueQueue []*V) +} + +// AggregatePriority caches the priority value from an aggregator for priority. +// - V is the value type used as a pointer +// - P is the priority type descending order, ie. Integer Floating-Point string +type AggregatePriority[V any, P constraints.Ordered] interface { + // Aggregator returns the aggregator associated with this AggregatePriority + Aggregator() (aggregator Aggregator[V, P]) + // Update caches the current priority from the aggregator + Update() + // Priority returns the effective cached priority + // - Priority is used by consumers of the AggregatingPriorityQueue + CachedPriority() (priority P) + // Index indicates insertion order + // - used for ordering elements of equal priority + Index() (index int) +} + +// Aggregator aggregates, snapshots and assigns priority to an associated value. +// - V is the value type used as a pointer +// - V may be a thread-safe object whose values change in real-time +// - P is the priority type descending order, ie. Integer Floating-Point string +type Aggregator[V any, P constraints.Ordered] interface { + // Value returns the value object this Aggregator is associated with + // - the Value method is used by consumers of the AggregatingPriorityQueue + Value() (valuep *V) + // Aggregate aggregates and snapshots data values from the value object. + // - Aggregate is invoked outside of AggregatingPriorityQueue + Aggregate() + // Priority returns the current priority for the associated value + // - this priority is cached by AggregatePriority + Priority() (priority P) +} + +// AssignedPriority contains the assigned priority for a priority-queue element +// - V is the element value type whose pointer-value provides identity +// - P is the priority, a descending-ordered type: Integer Floating-Point string +type AssignedPriority[V any, P constraints.Ordered] interface { + SetPriority(priority P) +} diff --git a/internal/cyclebreaker/annotation.go b/internal/cyclebreaker/annotation.go new file mode 100644 index 00000000..b67952cf --- /dev/null +++ b/internal/cyclebreaker/annotation.go @@ -0,0 +1,39 @@ +/* +© 2020–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package cyclebreaker + +import ( + "fmt" + + "github.com/haraldrudell/parl/pruntime" +) + +const ( + // counts the stack-frame of [parl.Annotation] + parlAnnotationFrames = 1 + // counts the stack-frame of [parl.getAnnotation] + getAnnotationFrames = 1 +) + +// Annotation provides a default reovered-panic code annotation +// - “Recover from panic in mypackage.MyFunc” +// - [base package].[function]: "mypackage.MyFunc" +func Annotation() (a string) { + return getAnnotation(parlAnnotationFrames) +} + +// getAnnotation provides a default reovered-panic code getAnnotation +// - frames = 0 means immediate caller of getAnnotation +// - “Recover from panic in mypackage.MyFunc” +// - [base package].[function]: "mypackage.MyFunc" +func getAnnotation(frames int) (a string) { + if frames < 0 { + frames = 0 + } + return fmt.Sprintf("Recover from panic in %s:", + pruntime.NewCodeLocation(frames+getAnnotationFrames).PackFunc(), + ) +} diff --git a/internal/cyclebreaker/closer.go b/internal/cyclebreaker/closer.go index e6cd1f87..c3e21153 100644 --- a/internal/cyclebreaker/closer.go +++ b/internal/cyclebreaker/closer.go @@ -15,7 +15,7 @@ import ( // Closer handles panics. // if errp is non-nil, panic values updates it using errors.AppendError. func Closer[T any](ch chan T, errp *error) { - defer Recover(Annotation(), errp, NoOnError) + defer Recover("", errp, NoOnError) close(ch) } @@ -24,7 +24,7 @@ func Closer[T any](ch chan T, errp *error) { // CloserSend handles panics. // if errp is non-nil, panic values updates it using errors.AppendError. func CloserSend[T any](ch chan<- T, errp *error) { - defer Recover(Annotation(), errp, NoOnError) + defer Recover("", errp, NoOnError) close(ch) } @@ -33,7 +33,7 @@ func CloserSend[T any](ch chan<- T, errp *error) { // Close handles panics. // if errp is non-nil, panic values updates it using errors.AppendError. func Close(closable io.Closer, errp *error) { - defer Recover(Annotation(), errp, NoOnError) + defer Recover("", errp, NoOnError) if e := closable.Close(); e != nil { *errp = perrors.AppendError(*errp, e) diff --git a/internal/cyclebreaker/ensure-error.go b/internal/cyclebreaker/ensure-error.go new file mode 100644 index 00000000..41c28ab8 --- /dev/null +++ b/internal/cyclebreaker/ensure-error.go @@ -0,0 +1,54 @@ +/* +© 2020–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package cyclebreaker + +import ( + "fmt" + + "github.com/haraldrudell/parl/perrors" +) + +const ( + // counts the stack frame of [parl.ensureError] + parlEnsureErrorFrames = 1 + // counts the stack frame of [parl.EnsureError] + parlEnsureErrorFrames0 = 1 +) + +// ensureError interprets a panic values as an error +// - returned value is either nil or an error value with stack trace +// - the error is ensured to have stack trace +func EnsureError(panicValue any) (err error) { + return ensureError(panicValue, parlEnsureErrorFrames0) +} + +// ensureError interprets a panic values as an error +// - returned value is either nil or an error value with stack trace +// - frames is used to select the stack frame from where the stack trace begins +// - frames 0 is he caller of ensure Error +func ensureError(panicValue any, frames int) (err error) { + + // no panic is no-op + if panicValue == nil { + return // no panic return + } + + // ensure value to be error + var ok bool + if err, ok = panicValue.(error); !ok { + err = fmt.Errorf("non-error value: %T %+[1]v", panicValue) + } + + // ensure stack trace + if !perrors.HasStack(err) { + if frames < 0 { + frames = 0 + } + err = perrors.Stackn(err, frames+parlEnsureErrorFrames) + } + + return +} diff --git a/internal/cyclebreaker/recover-da.go b/internal/cyclebreaker/recover-da.go new file mode 100644 index 00000000..8f839c03 --- /dev/null +++ b/internal/cyclebreaker/recover-da.go @@ -0,0 +1,48 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package cyclebreaker + +import "github.com/haraldrudell/parl/pruntime" + +const ( + // counts the frames in [parl.A] + parlAFrames = 1 +) + +// DA is the value returned by a deferred code location function +type DA *pruntime.CodeLocation + +// A is a thunk returning a deferred code location +func A() DA { return pruntime.NewCodeLocation(parlAFrames) } + +// RecoverDA recovers panic using deferred annotation +// +// Usage: +// +// func someFunc() (err error) { +// defer parl.RecoverDA(func() parl.DA { return parl.A() }, &err, parl.NoOnError) +func RecoverDA(deferredLocation func() DA, errp *error, onError OnError) { + doRecovery("", deferredLocation, errp, onError, recover2OnErrrorOnce, noIsPanic, recover()) +} + +// RecoverErr recovers panic using deferred annotation +// +// Usage: +// +// func someFunc() (isPanic bool, err error) { +// defer parl.RecoverErr(func() parl.DA { return parl.A() }, &err, &isPanic) +func RecoverErr(deferredLocation func() DA, errp *error, isPanic ...*bool) { + var isPanicp *bool + if len(isPanic) > 0 { + isPanicp = isPanic[0] + } + doRecovery("", deferredLocation, errp, NoOnError, recover2OnErrrorOnce, isPanicp, recover()) +} + +// RecoverDA2 recovers panic using deferred annotation +func RecoverDA2(deferredLocation func() DA, errp *error, onError OnError) { + doRecovery("", deferredLocation, errp, onError, recover2OnErrrorMultiple, noIsPanic, recover()) +} diff --git a/internal/cyclebreaker/recover-invocation-panic.go b/internal/cyclebreaker/recover-invocation-panic.go index 28366399..b4e0c513 100644 --- a/internal/cyclebreaker/recover-invocation-panic.go +++ b/internal/cyclebreaker/recover-invocation-panic.go @@ -22,7 +22,7 @@ func RecoverInvocationPanic(fn func(), errp *error) { if errp == nil { panic(perrors.ErrorfPF("%w", ErrErrpNil)) } - defer Recover(Annotation(), errp, NoOnError) + defer Recover("", errp, NoOnError) fn() } diff --git a/internal/cyclebreaker/recover.go b/internal/cyclebreaker/recover.go index 07918298..338c5573 100644 --- a/internal/cyclebreaker/recover.go +++ b/internal/cyclebreaker/recover.go @@ -7,54 +7,104 @@ package cyclebreaker import ( "fmt" + "strings" "github.com/haraldrudell/parl/perrors" "github.com/haraldrudell/parl/pruntime" ) const ( - recAnnStackFrames = 1 - recRecStackFrames = 2 - recEnsureErrorFrames = 2 - // Recover() and Recover2() are deferred functions invoked on panic - // Because the functions are directly invoked by runtime panic code, - // there are no intermediate stack frames between Recover*() and runtime.panic*. - // therefore, the Recover stack frame must be included in the error stack frames - // recover2() + processRecover() + ensureError() == 3 - recProcessRecoverFrames = 3 + panicString = ": panic:" + recover2OnErrrorOnce = false + recover2OnErrrorMultiple = true ) -// Recover recovers from a panic invoking a function no more than once. -// If there is *errp does not hold an error and there is no panic, onError is not invoked. -// Otherwise, onError is invoked exactly once. -// *errp is updated with a possible panic. -func Recover(annotation string, errp *error, onError func(error)) { - recover2(annotation, errp, onError, false, recover()) +const ( + // counts the stack-frame in [parl.processRecover] + processRecoverFrames = 1 + // counts the stack-frame of [parl.doRecovery] and [parl.Recover] or [parl.Recover2] + // - but for panic detectpr to work, there must be one frame after + // runtime.gopanic, so remove one frame + doRecoveryFrames = 2 - 1 +) + +// OnError is a function that receives error values from an errp error pointer or a panic +type OnError func(err error) + +// NoOnError is used with Recover and Recover2 to silence the default error logging +func NoOnError(err error) {} + +var noIsPanic *bool + +// Recover recovers from panic invoking onError exactly once with an aggregate error value +// - annotation may be empty, errp and onError may be nil +// - errors in *errp and panic are aggregated into a single error value +// - if onError non-nil, the function is invoked once with the aggregate error +// - if onError nil, the aggregate error is logged to standard error +// - if onError is [Parl.NoOnErrror], logging is suppressed +// - if errp is non-nil, it is updated with the aggregate error +// - if annotation is empty, a default annotation is used for the immediate caller of Recover +func Recover(annotation string, errp *error, onError OnError) { + doRecovery(annotation, nil, errp, onError, recover2OnErrrorOnce, noIsPanic, recover()) } -// Recover2 recovers from a panic and may invoke onError multiple times. -// onError is invoked if there is an error at *errp and on a possible panic. -// *errp is updated with a possible panic. -func Recover2(annotation string, errp *error, onError func(error)) { - recover2(annotation, errp, onError, true, recover()) +// Recover2 recovers from panic invoking onError for any eror in *errp and any panic +// - annotation may be empty, errp and onError may be nil +// - if onError non-nil, the function is invoked with any error in *errp and any panic +// - if onError nil, the errors are logged to standard error +// - if onError is [Parl.NoOnErrror], logging is suppressed +// - if errp is non-nil, it is updated with an aggregate error +// - if annotation is empty, a default annotation is used for the immediate caller of Recover +func Recover2(annotation string, errp *error, onError OnError) { + doRecovery(annotation, nil, errp, onError, recover2OnErrrorMultiple, noIsPanic, recover()) } -func recover2(annotation string, errp *error, onError func(error), multiple bool, recoverValue interface{}) { - // ensure non-empty annotation - if annotation == "" { - annotation = pruntime.NewCodeLocation(recRecStackFrames).PackFunc() + ": panic:" - } +// doRecovery implements recovery ffor Recovery andd Recovery2 +func doRecovery(annotation string, deferredAnnotation func() DA, errp *error, onError OnError, multiple bool, isPanic *bool, recoverValue interface{}) { - // consume *errp + // build aggregate error in err var err error + + // if onError is to be invoked multiple times, + // and *errp contains an error, + // invoke onError or Log to standard error if errp != nil { if err = *errp; err != nil && multiple { - invokeOnError(onError, err) + invokeOnError(onError, err) // invokee onError or parl.Log } } // consume recover() - if e := processRecover(annotation, recoverValue); e != nil { + if recoverValue != nil { + if isPanic != nil { + *isPanic = true + } + if annotation == "" { + if deferredAnnotation != nil { + if da := deferredAnnotation(); da != nil { + var cL = (*pruntime.CodeLocation)(da) + // single wword package name + var packageName = cL.Package() + // recoverDaPanic.func1: hosting function name and a derived name for the function literal + var funcName = cL.FuncIdentifier() + // removed “.func1” suffix + if index := strings.LastIndex(funcName, "."); index != -1 { + funcName = funcName[:index] + } + annotation = fmt.Sprintf("panic detected in %s.%s:", + packageName, + funcName, + ) + } + } + if annotation == "" { + // default annotation cannot be obtained + // - the deferred Recover function is invoked directly from rutine, eg. runtime.gopanic + // - therefore, use fixed string + annotation = "recover from panic:" + } + } + e := processRecover(annotation, recoverValue, doRecoveryFrames) if multiple { invokeOnError(onError, e) } else { @@ -62,7 +112,9 @@ func recover2(annotation string, errp *error, onError func(error), multiple bool } } - // write back to *errp, do non-multiple invocation + // if err now contains any error + // - write bacxk to non-nil errp + // - if not multiple, invoke onErorr or Log the aggregate error if err != nil { if errp != nil && *errp != err { *errp = err @@ -73,95 +125,24 @@ func recover2(annotation string, errp *error, onError func(error), multiple bool } } -func invokeOnError(onError func(error), err error) { +// invokeOnError invokes an onError function or logs to standard error if onError is nil +func invokeOnError(onError OnError, err error) { if onError != nil { onError(err) - } else { - Log("Recover: %+v\n", err) + return } -} - -// NoOnError is used with Recover to silence the default error logging -func NoOnError(err error) {} - -// Annotation provides a default annotation [base package].[function]: "mypackage.MyFunc" -func Annotation() (annotation string) { - return fmt.Sprintf("Recover from panic in %s:", pruntime.NewCodeLocation(recAnnStackFrames).PackFunc()) + Log("Recover: %+v\n", err) } // processRecover ensures non-nil result to be error with Stack -func processRecover(annotation string, panicValue interface{}) (err error) { - if err = ensureError(panicValue, recProcessRecoverFrames); err == nil { - return // panicValue nil return: no error - } - - // annotate - if annotation != "" { - err = perrors.Errorf("%s \x27%w\x27", annotation, err) - } - return -} - -// AddToPanic ensures that a recover() value is an error or nil. -func EnsureError(panicValue interface{}) (err error) { - return ensureError(panicValue, recEnsureErrorFrames) -} - -func ensureError(panicValue interface{}, frames int) (err error) { - - if panicValue == nil { - return // no panic return - } - - // ensure value to be error - var ok bool - if err, ok = panicValue.(error); !ok { - err = fmt.Errorf("non-error value: %T %+[1]v", panicValue) - } - - // ensure stack trace - if !perrors.HasStack(err) { - err = perrors.Stackn(err, frames) - } - - return -} - -// AddToPanic takes a recover() value and adds it to additionalErr. -func AddToPanic(panicValue interface{}, additionalErr error) (err error) { - if err = EnsureError(panicValue); err == nil { - return additionalErr - } - if additionalErr == nil { - return +// - annotation is non-empty annotation indicating code loction or action +// - panicValue is non-nil value returned by built-in recover function +func processRecover(annotation string, panicValue interface{}, frames int) (err error) { + if frames < 0 { + frames = 0 } - return perrors.AppendError(err, additionalErr) -} - -// HandlePanic recovers from panic in fn returning error. -func HandlePanic(fn func()) (err error) { - defer Recover(Annotation(), &err, nil) - - fn() - return + return perrors.Errorf("%s “%w”", + annotation, + ensureError(panicValue, frames+processRecoverFrames), + ) } - -// HandleErrp recovers from a panic in fn storing at *errp. -// HandleErrp is deferable. -func HandleErrp(fn func(), errp *error) { - defer Recover(Annotation(), errp, nil) - - fn() -} - -// HandleParlError recovers from panic in fn invoking an error callback. -// HandleParlError is deferable -// storeError can be the thread-safe perrors.ParlError.AddErrorProc() -func HandleParlError(fn func(), storeError func(err error)) { - defer Recover(Annotation(), nil, storeError) - - fn() -} - -// perrors.ParlError.AddErrorProc can be used with HandleParlError -var _ func(err error) = (&perrors.ParlError{}).AddErrorProc diff --git a/invoke-timer-invo.go b/invoke-timer-invo.go index 1c3a3b67..422edb89 100644 --- a/invoke-timer-invo.go +++ b/invoke-timer-invo.go @@ -47,6 +47,7 @@ func (i *invokeTimerInvo) deferFunc() { } // oldestFirst is an order function sorting the oldest invocation first -func (i *invokeTimerInvo) oldestFirst(a, b *invokeTimerInvo) (result int) { - return a.t0.Compare(b.t0) +// - oldest is before other times +func (i *invokeTimerInvo) oldestFirst(a, b *invokeTimerInvo) (aBeforeB bool) { + return a.t0.Compare(b.t0) == -1 } diff --git a/iters/base-iterator.go b/iters/base-iterator.go new file mode 100644 index 00000000..b8613951 --- /dev/null +++ b/iters/base-iterator.go @@ -0,0 +1,283 @@ +/* +© 2022-present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +All rights reserved +*/ + +package iters + +import ( + "errors" + "sync" + "sync/atomic" + + "github.com/haraldrudell/parl/internal/cyclebreaker" + "github.com/haraldrudell/parl/perrors" + "github.com/haraldrudell/parl/plog" +) + +const ( + // invoke iteratorFunction for next value + enqueueForFnNextItem = false + // invoke iteratorFunction to request cancel + enqueueForFnCancel = true +) + +// baseIterator is a pointed-to enclosed type due to BaseIterator providing +// baseIterator.delegateAction +type baseIterator[T any] struct { + invokeFn InvokeFunc[T] + + // publicsLock serializes invocations of i.Next and i.Cancel + publicsLock sync.Mutex + + // Next + + // didNext indicates that a Next operation has completed and that hasValue may be valid + // - behind publicsLock + didNext bool + // value is a returned value from fn. + // value is valid when isEnd is false and hasValue is true. + // - behind publicsLock + value T + // hasValue indicates that value is valid. + // - behind publicsLock + hasValue bool + + // indicates that no further values can be returned. + // caused by: + // - error received from iterator + // - iterator cancel completed + // - iterator signaled end of values + // - — + // - written behind publicsLock + noValuesAvailable atomic.Bool + + // enqueueForFn + + // cancelState is updated by: + // - Cancel() and + // - enqueueForFn() inside i.publicsLock lock + cancelState atomic.Uint32 + // errWait waits until + // - an error is received from fn + // - cancel returns from fn FunctionIteratorCancel -1 invocation + // without error + // - fn signals end of data ny returning parl.ErrEndCallbacks + // - updated by enqueueForFn() inside i.publicsLock lock + errWait sync.WaitGroup + // err contains any errors returned by fn other than parl.ErrEndCallbacks + // - nil if fn is active + // - non-nil pointer to nil error if fn completed without error + // - — returned parl.ErrEndCallbacks + // - — returned from FunctionIteratorCancel -1 invocation without error + // - non-nil error if fn returned error + // - updated by enqueueForFn() inside i.publicsLock lock + err atomic.Pointer[error] +} + +// BaseIterator implements the DelegateAction[T] function required by +// Delegator[T] +type BaseIterator[T any] struct { + // baseIterator is a pointed-to enclosed type due to BaseIterator providing + // baseIterator.delegateAction + *baseIterator[T] +} + +// NewFunctionIterator returns a parli.Iterator based on a function. +// functionIterator is thread-safe and re-entrant. +func NewBaseIterator[T any]( + invokeFn InvokeFunc[T], + delegateActionReceiver *DelegateAction[T], +) (iterator *BaseIterator[T]) { + if invokeFn == nil { + panic(perrors.NewPF("invokeFn cannot be nil")) + } else if delegateActionReceiver == nil { + panic(perrors.NewPF("delegateActionReceiver cannot be nil")) + } + + // pointer to baseIterator.delegateAction so this method can + // be provided + // - also causes werrWait to be pointed to, a used non-copy struct + i := baseIterator[T]{invokeFn: invokeFn} + i.errWait.Add(1) + *delegateActionReceiver = i.delegateAction + + return &BaseIterator[T]{baseIterator: &i} +} + +// delegateAction finds the next or the same value +// - isSame true means first or same value should be returned +// - value is the sought value or the T type’s zero-value if no value exists +// - hasValue true means value was assigned a valid T value +func (i *baseIterator[T]) delegateAction(isSame NextAction) (value T, hasValue bool) { + + // fast outside-lock value-check + if i.noValuesAvailable.Load() { + return // no more values: zero-value and hasValue false + } + // wait for access to fn + i.publicsLock.Lock() + defer i.publicsLock.Unlock() + + // inside-lock check + if i.noValuesAvailable.Load() { + return // no more values: zero-value and hasValue false + } + if cancelStates(i.cancelState.Load()) != notCanceled { + i.noValuesAvailable.Store(true) + return // some cancellation of the iterator + } + + // same value when a value has previously been read + if i.didNext { + if isSame == IsSame { + value = i.value + hasValue = i.hasValue + return // same value return + } + } else { + // did seek the first value + i.didNext = true + } + + // invoke fn + var cancelState cancelStates + var err error + // enqueueForFn unlocks while invoking fn to allow re-entrancy. + // if fn returns error, enqueueForFn updates: iter.isEnd iter.err iter.value iter.hasValue. + value, cancelState, err = i.enqueueForFn(enqueueForFnNextItem) + + // determine whether a value was provided + hasValue = err == nil && cancelState == notCanceled + + if !hasValue { + i.noValuesAvailable.Store(true) + return // no more values available + } + + // store received value for future IsSame + i.value = value + i.hasValue = hasValue + + return // hasValue true, valid value return +} + +// Cancel release resources for this iterator. +// Not every iterator requires a Cancel invocation. +func (i *baseIterator[T]) Cancel() (err error) { + + // ignore if cancel alread invoked + if !i.cancelState.CompareAndSwap(uint32(notCanceled), uint32(cancelRequested)) { + i.errWait.Wait() + err = *i.err.Load() + return // already beyond cancel + } + + // wait for access to fn + i.publicsLock.Lock() + defer i.publicsLock.Unlock() + + _, _, err = i.enqueueForFn(enqueueForFnCancel) + + return +} + +// enqueueForFn invokes iter.fn +// - is hasValue true, value is valid, err nil, cancelState notCanceled +// - otherwise, i.err is updated, i.errWait released, +// cancelState other than notCanceled or cancelRequested +// - — +// - invoked while holding i.publicsLock +// - recovers from fn panics +// - updates i.err i.cancelState i.errWait +func (i *baseIterator[T]) enqueueForFn(isCancel bool) ( + value T, + cancelState cancelStates, + err error, +) { + + // cancelState immediately prior to invoking i.invokeFn + cancelState = cancelStates(i.cancelState.Load()) + + // is invocation is still possible? + if cancelState != notCanceled && + cancelState != cancelRequested { + err = *i.err.Load() // collect previous error + return // no-further-invocations state return + } + // true if a panic occurred here or in invokeFn + // - false: invokeFn returned + var isPanic bool + // true if invokeFn decided to cancel istead of request a value + var didCancel bool + // deferred update of i.cancelState i.err i.errWait + defer i.updateState(&cancelState, &didCancel, &err) + // capture panics + defer cyclebreaker.RecoverErr(func() cyclebreaker.DA { return cyclebreaker.A() }, &err, &isPanic) + + // invoke i.invokeFn + + // wasCancel is the cancel value provided to invokeFn + var wasCancel = cancelState != notCanceled + // the value returned by invokeFn + var v T + + v, didCancel, isPanic, err = i.invokeFn(wasCancel) + + // determine if value is valid + if err == nil && !wasCancel && !didCancel { + value = v + } + + return +} + +// update i.cancelState i.err i.errWait +func (i *baseIterator[T]) updateState(cancelStatep *cancelStates, didCancelp *bool, errp *error) { + // cancelState immediately prior to invoking i.invokeFn + // - either notCanceled cancelRequested + var cancelState = *cancelStatep + // whether invokeFn decided to cancel iteration + var didCancel = *didCancelp + // possible error returned by i.invokeFn or result of panic + var err = *errp + + plog.D("updateState: cancelState %s didCancel %t err %s", + cancelState, didCancel, perrors.Short(err), + ) + + // determine next state and ignore ErrEndCallbacks + var nextState = notCanceled + if err != nil { + + // received a bad error from i.invokeFn + if !errors.Is(err, cyclebreaker.ErrEndCallbacks) { + nextState = errorReceived + } else { + + // received end-of-data from i.invokeFn + err = nil // ignore the error + if cancelState == notCanceled { + nextState = endOfData // spontaneous end of data + } else { + nextState = cancelComplete // non-error return from cancel invocation + } + } + } else if cancelState == cancelRequested || didCancel { + + // non-error return from cancel invocation + nextState = cancelComplete + } + + // handle transition to ended state + if nextState != notCanceled { + // update the ended state + i.cancelState.Store(uint32(nextState)) + *cancelStatep = nextState // update return value + // try and store error + if i.err.CompareAndSwap(nil, &err) { + // if error update, notify threads awaiting error + i.errWait.Done() + } + } +} diff --git a/iters/cancel-states.go b/iters/cancel-states.go new file mode 100644 index 00000000..c2dcf107 --- /dev/null +++ b/iters/cancel-states.go @@ -0,0 +1,42 @@ +/* +© 2022-present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +All rights reserved +*/ + +package iters + +import "fmt" + +const ( + // iteration is in progress + notCanceled cancelStates = iota + // consumer has invoked Cancel + cancelRequested + // cancel was successfully requested from the iterator-value + // producer + cancelComplete + // end-of-data notice received from value-producer, + // typically by returning parl.ErrEndCallbacks + endOfData + // value-producer returned error other than parl.ErrEndCallbacks + errorReceived +) + +// notCanceled cancelRequested cancelComplete endOfData errorReceived +type cancelStates uint32 + +var cancelStatesMap = map[cancelStates]string{ + notCanceled: "notCanceled", + cancelRequested: "cancelRequested", + cancelComplete: "cancelComplete", + endOfData: "endOfData", + errorReceived: "errorReceived", +} + +func (s cancelStates) String() (s2 string) { + s2 = cancelStatesMap[s] + if s2 == "" { + s2 = fmt.Sprintf("?badCancelState:%d", s) + } + return +} diff --git a/iters/converter-iterator.go b/iters/converter-iterator.go index bd7783e1..cb60980f 100644 --- a/iters/converter-iterator.go +++ b/iters/converter-iterator.go @@ -6,167 +6,103 @@ All rights reserved package iters import ( - "errors" - "sync" - "github.com/haraldrudell/parl/internal/cyclebreaker" "github.com/haraldrudell/parl/perrors" "golang.org/x/exp/constraints" ) -// ConverterIterator traverses another iterator and returns converted values. Thread-safe. -type ConverterIterator[K constraints.Ordered, T any] struct { - lock sync.Mutex - // nextCounter counts and identifies Next operations - nextCounter int +// converterIterator is an enclosed implementation type for +// ConverterIterator[K, V] +type converterIterator[K constraints.Ordered, V any] struct { + // keyIterator provides the key values converterFunction uses to + // return values keyIterator Iterator[K] - // fn receives a key and returns the corresponding value. - // if fn returns error, it will not be invoked again. - // if isCancel is true, this is the final invocation of fn and it should release any resource. - // if isCancel is true, no value is sought. - // if fn returns parl.ErrEndCallbacks, it will not be invoked again. - fn func(key K, isCancel bool) (value T, err error) - // fnEnded indicates that fn reurned parl.ErrEndCallbacks or other error. - // when fnEnded is true, fn does not need isCancel invocation. - fnEnded bool - value T - hasValue bool - err error - // isCancel indicates that Cancel was invoked. - // isCancel invalidates both value and hasValue. - isCancel bool + // ConverterFunction receives a key and returns the corresponding value. + // - if isCancel true, it means this is the last invocation of ConverterFunction and + // ConverterFunction should release any resources. + // Any returned value is not used + // - ConverterFunction signals end of values by returning parl.ErrEndCallbacks. + // if hasValue true, the accompanying value is used + // - if ConverterFunction returns error, it will not be invoked again. + // Any returned value is not used + // - ConverterFunction must be thread-safe + // - ConverterFunction is invoked by at most one thread at a time + converterFunction ConverterFunction[K, V] } -// NewConverterIterator returns a converting iterator. -// ConverterIterator is trhread-safe and re-entrant. -func NewConverterIterator[K constraints.Ordered, T any]( - keyIterator Iterator[K], fn func(key K, isCancel bool) (value T, err error)) (iterator Iterator[T]) { - if fn == nil { - panic(perrors.NewPF("fn cannot be nil")) - } else if keyIterator == nil { - panic(perrors.NewPF("keyIterator cannot be nil")) - } - return &Delegator[T]{Delegate: &ConverterIterator[K, T]{ - keyIterator: keyIterator, - fn: fn, - }} +// ConverterIterator traverses another iterator and returns converted values. Thread-safe. +type ConverterIterator[K constraints.Ordered, V any] struct { + // converterIterator implements the invokeConverterFunction method + // - pointer since invokeConverterFunction method is provided to + // BaseIterator + *converterIterator[K, V] + // BaseIterator implements Cancel and the DelegateAction[T] function required by + // Delegator[T] + // - receives invokeConverterFunction function + // - provides delegateAction function + BaseIterator[V] + // Delegator implements the value methods required by the [Iterator] interface + // - Next HasNext NextValue + // Same Has SameValue + // - receives DelegateAction[T] function + Delegator[V] } -// InitConverterIterator initializes a ConverterIterator struct. -// ConverterIterator is trhread-safe and re-entrant. -func InitConverterIterator[K constraints.Ordered, T any]( - iterp *ConverterIterator[K, T], +// NewConverterIterator returns a converting iterator. +// - ConverterIterator is thread-safe and re-entrant. +func NewConverterIterator[K constraints.Ordered, V any]( keyIterator Iterator[K], - fn func(key K, isCancel bool) (value T, err error), -) { - if iterp == nil { - panic(perrors.NewPF("iterator cannot be nil")) - } - if keyIterator == nil { - panic(perrors.NewPF("keyIterator cannot be nil")) - } - if fn == nil { + converterFunction ConverterFunction[K, V], +) (iterator Iterator[V]) { + if converterFunction == nil { panic(perrors.NewPF("fn cannot be nil")) - } - iterp.lock = sync.Mutex{} - iterp.nextCounter = 0 - iterp.keyIterator = keyIterator - iterp.fn = fn - iterp.fnEnded = false - var value T - iterp.value = value - iterp.hasValue = false - iterp.err = nil - iterp.isCancel = false -} - -func (iter *ConverterIterator[K, T]) Next(isSame NextAction) (value T, hasValue bool) { - iter.lock.Lock() - defer iter.lock.Unlock() - - // should next operation be invoked? - if iter.isCancel || iter.fnEnded { - return // error from fn or cancel invoked: zero-value and hasValue false - } - - // same value when a value has previously been read - if isSame == IsSame && iter.nextCounter != 0 { - value = iter.value - hasValue = iter.hasValue - return - } - - // next operation begins… - nextID := iter.nextCounter - iter.nextCounter++ - - // get next key from keyIterator - key, hasKey := iter.keyIterator.Next() - if !hasKey { - return // no more items return: zero-value, false + } else if keyIterator == nil { + panic(perrors.NewPF("keyIterator cannot be nil")) } - // get value for this key - var err error - if value, err = iter.convert(key); err != nil { - iter.fnEnded = true - if !errors.Is(err, cyclebreaker.ErrEndCallbacks) { - iter.err = perrors.AppendError(iter.err, err) - } - var zeroValue T - value = zeroValue - iter.value = zeroValue - iter.hasValue = false - return // failed conversion error return: zero-value, false + c := converterIterator[K, V]{ + keyIterator: keyIterator, + converterFunction: converterFunction, } - hasValue = true - if nextID+1 == iter.nextCounter { - iter.value = value - iter.hasValue = true + var delegateAction DelegateAction[V] + return &ConverterIterator[K, V]{ + converterIterator: &c, + BaseIterator: *NewBaseIterator(c.invokeConverterFunction, &delegateAction), + Delegator: *NewDelegator(delegateAction), } - - return // good return: value and hasValue true } -func (iter *ConverterIterator[K, T]) convert(key K) (value T, err error) { - iter.lock.Unlock() - defer iter.lock.Lock() - - cyclebreaker.RecoverInvocationPanic(func() { - value, err = iter.fn(key, false) - }, &err) - return -} - -func (iter *ConverterIterator[K, T]) Cancel() (err error) { - iter.lock.Lock() - defer iter.lock.Unlock() - - // if already error, or Cancel has alredy been invoked, return same result - if iter.isCancel { - err = iter.err - return - } - iter.isCancel = true +// invokeConverterFunction invokes converterFunction recovering a possible panic +// - if cancelState == notCanceled, a new value is requested. +// Otherwise, iteration cancel is requested +// - if err is nil, value is valid and isPanic false. +// Otherwise, err is non-nil and isPanic may be set. +// value is zero-value +func (i *converterIterator[K, T]) invokeConverterFunction(isCancel bool) ( + value T, + didCancel bool, + isPanic bool, + err error, +) { + defer cyclebreaker.RecoverErr(func() cyclebreaker.DA { return cyclebreaker.A() }, &err, &isPanic) - // ensure fn canceled and have any errors in err - if !iter.fnEnded { - cyclebreaker.RecoverInvocationPanic(func() { - var k K - _, err = iter.fn(k, true) - }, &err) + // get next key from keyIterator + var key K + if !isCancel { + var hasKey bool + key, hasKey = i.keyIterator.Next() + isCancel = !hasKey + didCancel = !hasKey } - err = perrors.AppendError(iter.err, err) - // cancel keyIterator, append to err - if err2 := iter.keyIterator.Cancel(); err2 != nil { - err = perrors.AppendError(err, err2) - } + var v T + // invoke converter function + v, err = i.converterFunction(key, isCancel) - // store error result - if err != nil { - iter.err = err + // determine iof value is valid + if err == nil && !isCancel { + value = v } return diff --git a/iters/converter-iterator_test.go b/iters/converter-iterator_test.go index 6d621954..232c9d82 100644 --- a/iters/converter-iterator_test.go +++ b/iters/converter-iterator_test.go @@ -6,154 +6,85 @@ All rights reserved package iters import ( - "errors" - "strings" "testing" - - "github.com/haraldrudell/parl/perrors" ) func TestConverterIterator(t *testing.T) { - valueK := "value" - valueT := 17 - iterErr := errors.New("iter-err") - errBadK := errors.New("bad K") - errFnShouldFail := errors.New("fnShouldError") - - var err error - var iter Iterator[int] - var slice = []string{valueK} - var actualT int - var hasValue bool - var zeroValueT int - var fnShouldError bool - fn := func(key string, isCancel bool) (value int, err error) { - if fnShouldError { - fnShouldError = false - err = errFnShouldFail - return + // keys that the converter-iterator will iterate over + var keys = []string{"z", "keyTwo"} + // the expected values produces by the converter iterator + var values = func(keys []string) (values []int) { + values = make([]int, len(keys)) + for i, key := range keys { + values[i] = len(key) } - if isCancel { - return - } - if key != valueK { - err = errBadK - return - } - value = valueT return - } + }(keys) - // test methods + t.Logf("keys: %v", keys) + t.Logf("values: %v", values) - // Next - iter = NewConverterIterator(NewSliceIterator(slice), fn) - // Next1: exhaust keys - if actualT, hasValue = iter.Next(); !hasValue { - t.Error("Next1 hasValue false") - } - if actualT != valueT { - t.Errorf("Next1 %d exp: %d", actualT, valueT) - } - // Next2: exhaust keys - if actualT, hasValue = iter.Next(); hasValue { - t.Error("Next2 hasValue true") - } - if actualT != zeroValueT { - t.Errorf("Next2 %d exp: %d", actualT, zeroValueT) - } - // Next3: fn error - fnShouldError = true - iter = NewConverterIterator(NewSliceIterator(slice), fn) - if actualT, hasValue = iter.Next(); hasValue { - t.Error("Next3 hasValue true") - } - if actualT != zeroValueT { - t.Errorf("Next3 %d exp: %d", actualT, zeroValueT) - } - //Next4 - if actualT, hasValue = iter.Next(); hasValue { - t.Error("Next4 hasValue true") + var keyIterator = NewSliceIterator(keys) + var value int + var hasValue bool + var zeroValue int + var err error + + var iterator Iterator[int] = NewConverterIterator( + keyIterator, + converterFunction, + ) + + // Same should advance to the first value + value, hasValue = iterator.Same() + //hasValue should be true + if !hasValue { + t.Error("Same hasValue false") } - if actualT != zeroValueT { - t.Errorf("Next4 %d exp: %d", actualT, zeroValueT) + // value should be first value + if value != values[0] { + t.Errorf("Same value %q exp %q", value, values[0]) } - // SameValue - iter = NewConverterIterator(NewSliceIterator(slice), fn) - if actualT = iter.SameValue(); actualT != valueT { - t.Errorf("SameValue %d exp: %d", actualT, valueT) + // Next should return the second value + value, hasValue = iterator.Next() + if !hasValue { + t.Errorf("Next hasValue false") } - // SameValue2 - if actualT = iter.SameValue(); actualT != valueT { - t.Errorf("SameValue2 %d exp: %d", actualT, valueT) + if value != values[1] { + t.Errorf("Next value %q exp %q", value, values[1]) } - // Cancel - iter = NewConverterIterator(NewSliceIterator(slice), fn) - delegator := iter.(*Delegator[int]) - converterItertor := delegator.Delegate.(*ConverterIterator[string, int]) - converterItertor.err = iterErr - if err = iter.Cancel(); err != iterErr { - t.Errorf("Cancel1 err: '%v' exp: '%v'", err, iterErr) + // Next should return no value + value, hasValue = iterator.Next() + if hasValue { + t.Errorf("Next2 hasValue true") } - if err = iter.Cancel(); err != iterErr { - t.Errorf("Cancel2 err: '%v' exp: '%v'", err, iterErr) + if value != zeroValue { + t.Errorf("Next2 value %q exp %q", value, zeroValue) } - iter = NewConverterIterator(NewSliceIterator(slice), func(key string, isCancel bool) (value int, err error) { - return - }) - if err = iter.Cancel(); err != nil { - t.Errorf("Cancel3 err: '%v''", err) + + // cancel should not return error + if err = iterator.Cancel(); err != nil { + t.Errorf("Cancel err '%v'", err) } } -func TestNewConverterIterator(t *testing.T) { - valueK := "value" - messageFnNil := "fn cannot be nil" - messageKeytIteratorNil := "keyIterator cannot be nil" +// type ConverterFunction[K constraints.Ordered, V any] +// func(key K, isCancel bool) (value V, err error) +var _ ConverterFunction[string, int] = converterFunction - var slice = []string{valueK} - var err error - fn := func(key string, isCancel bool) (value int, err error) { return } - - NewConverterIterator(NewSliceIterator(slice), fn) - - func() { - defer func() { - if v := recover(); v != nil { - var ok bool - if err, ok = v.(error); !ok { - err = perrors.Errorf("panic-value not error; %T '%[1]v'", v) - } - } - }() - NewConverterIterator[string, int](nil, nil) - }() - if err == nil || !strings.Contains(err.Error(), messageFnNil) { - t.Errorf("NewConverterIterator incorrect panic: '%v' exp %q", err, messageFnNil) - } +// converterFunction can be used with a +// ConverterIterator as ConverterFunction[string, int] +func converterFunction(key string, isCancel bool) (value int, err error) { - func() { - defer func() { - if v := recover(); v != nil { - var ok bool - if err, ok = v.(error); !ok { - err = perrors.Errorf("panic-value not error; %T '%[1]v'", v) - } - } - }() - NewConverterIterator(nil, fn) - }() - if err == nil || !strings.Contains(err.Error(), messageKeytIteratorNil) { - t.Errorf("NewConverterIterator incorrect key panic: '%v' exp %q", err, messageKeytIteratorNil) + // handle cancel + if isCancel { + return } -} -func TestInitConverterIterator(t *testing.T) { - keyIterator := NewEmptyIterator[string]() - var iterp ConverterIterator[string, int] - fn := func(key string, isCancel bool) (value int, err error) { return } + // produce value corresponding to key + value = len(key) - InitConverterIterator(&iterp, keyIterator, fn) + return } diff --git a/iters/delegate.go b/iters/delegate.go index 851c1a2d..7e9c1d8f 100644 --- a/iters/delegate.go +++ b/iters/delegate.go @@ -5,14 +5,45 @@ All rights reserved package iters +const ( + // IsSame indicates to Delegate.Next that + // this is a Same-type incovation + IsSame NextAction = 0 + // IsNext indicates to Delegate.Next that + // this is a Next-type incovation + IsNext NextAction = 1 +) + +// NextAction is a unique named type that indicates whether +// the next or the same value again is sought by Delegate.Next +// - IsSame IsNext +type NextAction uint8 + +// DelegateAction finds the next or the same value. +// - isSame == IsSame returns the same value again, +// finding the first value if a value has yet to be retrieved +// - isSame == IsNext find the next val;ue if one exists +// - value is the sought value or the T type’s zero-value if no value exists. +// - hasValue indicates whether value was assigned a T value. +type DelegateAction[T any] func(isSame NextAction) (value T, hasValue bool) + // Delegate defines the methods that an iterator implementation must implement // to use iterator.Delegator +// - Iterator methods: +// Next HasNext NextValue +// Same Has SameValue +// Cancel +// - Delegator Methods: +// Next HasNext NextValue +// Same Has SameValue +// - Delegate Methods +// Next Cancel type Delegate[T any] interface { // Next finds the next or the same value. // isSame indicates what value is sought. // value is the sought value or the T type’s zero-value if no value exists. // hasValue indicates whether value was assigned a T value. - Next(isSame NextAction) (value T, hasValue bool) + Action(isSame NextAction) (value T, hasValue bool) // Cancel indicates to the iterator implementation that iteration has concluded // and resources should be released. Cancel() (err error) diff --git a/iters/delegator.go b/iters/delegator.go index d43073e0..547eb466 100644 --- a/iters/delegator.go +++ b/iters/delegator.go @@ -5,42 +5,65 @@ All rights reserved package iters -// Delegator adds delegating methods that implements the Iterator interface. -// Delegator is thread-safe if its delegate is thread-safe. +import "github.com/haraldrudell/parl/perrors" + +// Delegator implements the value methods required by the [Iterator] interface +// - Next HasNext NextValue +// Same Has SameValue +// - the delegate provides DelegateAction[T] function +// - — delegate must be thread-safe +// - methods provided by Delegator: Has HasNext NextValue SameValue Same +// - delegate and Delegator combined fully implement the Iterator interface +// - Delegator is thread-safe type Delegator[T any] struct { - Delegate[T] + delegateAction DelegateAction[T] } -// NewDelegator returns a parli.Iterator based on a Delegate iterator implementation. +// NewDelegator returns an [Iterator] based on a Delegate iterator implementation. // Delegator is thread-safe if its delegate is thread-safe. -func NewDelegator[T any](delegate Delegate[T]) (iter Iterator[T]) { - return &Delegator[T]{Delegate: delegate} -} +func NewDelegator[T any](delegateAction DelegateAction[T]) (delegator *Delegator[T]) { + if delegateAction == nil { + perrors.NewPF("delegateAction cannot be nil") + } -func (iter *Delegator[T]) Next() (value T, hasValue bool) { - return iter.Delegate.Next(IsNext) + return &Delegator[T]{delegateAction: delegateAction} } -func (iter *Delegator[T]) HasNext() (ok bool) { - _, ok = iter.Next() +// Next advances to next item and returns it. +// - if the next item does exist, value is valid and hasValue is true. +// - if no next item exists, value is the data type zero-value and hasValue is false. +func (d *Delegator[T]) Next() (value T, hasValue bool) { return d.delegateAction(IsNext) } + +// HasNext advances to next item and returns hasValue true if this next item does exists. +func (d *Delegator[T]) HasNext() (hasValue bool) { + _, hasValue = d.Next() return } -func (iter *Delegator[T]) NextValue() (value T) { - value, _ = iter.Next() +// NextValue advances to next item and returns it +// - If no next value exists, the data type zero-value is returned. +func (d *Delegator[T]) NextValue() (value T) { + value, _ = d.Next() return } -func (iter *Delegator[T]) Same() (value T, hasValue bool) { - return iter.Delegate.Next(IsSame) -} +// Same returns the same value again +// - If a value does exist, it is returned in value and hasValue is true. +// - If a value does not exist, the data type zero-value is returned and hasValue is false. +// - If Next, FindNext or HasNext have not been invoked, Same first advances to the first item. +func (d *Delegator[T]) Same() (value T, hasValue bool) { return d.delegateAction(IsSame) } -func (iter *Delegator[T]) Has() (hasValue bool) { - _, hasValue = iter.Same() +// Has returns true if Same() or SameValue will return items +// - If Next, FindNext or HasNext have not been invoked, Has first advances to the first item. +func (d *Delegator[T]) Has() (hasValue bool) { + _, hasValue = d.Same() return } -func (iter *Delegator[T]) SameValue() (value T) { - value, _ = iter.Same() +// SameValue returns the same value again +// - If a value does not exist, the data type zero-value is returned. +// - If Next, FindNext or HasNext have not been invoked, SameValue first advances to the first item. +func (d *Delegator[T]) SameValue() (value T) { + value, _ = d.Same() return } diff --git a/iters/empty-iterator.go b/iters/empty-iterator.go index 3c2f31d5..ce7ac2e6 100644 --- a/iters/empty-iterator.go +++ b/iters/empty-iterator.go @@ -9,11 +9,8 @@ package iters type EmptyIterator[T any] struct{} // NewEmptyIterator returns an empty iterator of values type T. -// EmptyIterator is thread-safe. -func NewEmptyIterator[T any]() (iterator Iterator[T]) { - return &EmptyIterator[T]{} -} - +// - EmptyIterator is thread-safe. +func NewEmptyIterator[T any]() (iterator Iterator[T]) { return &EmptyIterator[T]{} } func (iter *EmptyIterator[T]) Next() (value T, hasValue bool) { return } func (iter *EmptyIterator[T]) HasNext() (ok bool) { return } func (iter *EmptyIterator[T]) NextValue() (value T) { return } diff --git a/iters/function-iterator.go b/iters/function-iterator.go index da2284c0..5a85ad22 100644 --- a/iters/function-iterator.go +++ b/iters/function-iterator.go @@ -6,154 +6,81 @@ All rights reserved package iters import ( - "errors" - "sync" - "github.com/haraldrudell/parl/internal/cyclebreaker" "github.com/haraldrudell/parl/perrors" ) -const ( - FunctionIteratorCancel int = -1 -) +// functionIterator is an enclosed implementation type for +// FunctionIterator[T] +type functionIterator[T any] struct { + // IteratorFunction is a function that can be used with function iterator + // - if isCancel true, it means this is the last invocation of IteratorFunction and + // IteratorFunction should release any resources. + // Any returned value is not used + // - IteratorFunction signals end of values by returning parl.ErrEndCallbacks. + // if hasValue true, the accompanying value is used + // - if IteratorFunction returns error, it will not be invoked again. + // Any returned value is not used + // - IteratorFunction must be thread-safe + // - IteratorFunction is invoked by at most one thread at a time + iteratorFunction IteratorFunction[T] +} -// FunctionIterator traverses a function generatoing values. thread-safe and re-entrant. +// FunctionIterator traverses a function generating values type FunctionIterator[T any] struct { - lock sync.Mutex - // didNext indicates that a Next operation has completed and that hasValue may be valid. - didNext bool - // index contains the next index to use for fn invocations - index int - // fn will be invoked with an index 0… until fn returns an error. - // if fn returns error, it will not be invoked again. - // fn signals end of values by returning parl.ErrEndCallbacks. - // when fn returns parl.ErrEndCallbacks, the accompanying value is not used. - // if index == FunctionIteratorCancel, it means Cancel. - // It is the last invocation of fn and fn should release any resources. - // fn is typically expected to be re-entrant and thread-safe. - fn func(index int) (value T, err error) - // value is a returned value from fn. - // value is valid when isEnd is false and hasValue is true. - value T - // err contains any errors returned by fn other than parl.ErrEndCallbacks - err error - // isEnd indicates that fn returned error or Cancel was invoked. - // isEnd invalidates both value and hasValue. - isEnd bool - // hasValue indicates that value is valid. - hasValue bool + // functionIterator invokes IteratorFunction[T] + // - pointer since it provides its invokeFn method to BaseIterator + *functionIterator[T] + // BaseIterator implements the DelegateAction[T] function required by + // Delegator[T] and Cancel + // - provides its delegateAction method to Delegator + BaseIterator[T] + // Delegator implements the value methods required by the [Iterator] interface + // - Next HasNext NextValue + // Same Has SameValue + // - Delegator obtains items using the provided DelegateAction[T] function + Delegator[T] } -// NewFunctionIterator returns a parli.Iterator based on a function. -// functionIterator is thread-safe and re-entrant. +// NewFunctionIterator returns an [Iterator] iterating over a function +// - thread-safe func NewFunctionIterator[T any]( - fn func(index int) (value T, err error), + iteratorFunction IteratorFunction[T], ) (iterator Iterator[T]) { - if fn == nil { + if iteratorFunction == nil { panic(perrors.NewPF("fn cannot be nil")) } - return &Delegator[T]{Delegate: &FunctionIterator[T]{fn: fn}} -} - -// InitFunctionIterator initializes a FunctionIterator struct. -// functionIterator is thread-safe and re-entrant. -func InitFunctionIterator[T any](iterp *FunctionIterator[T], fn func(index int) (value T, err error)) { - if iterp == nil { - panic(perrors.NewPF("iterator cannot be nil")) - } - if fn == nil { - panic(perrors.NewPF("fn cannot be nil")) - } - iterp.lock = sync.Mutex{} - iterp.didNext = false - iterp.index = 0 - iterp.fn = fn - var value T - iterp.value = value - iterp.err = nil - iterp.isEnd = false - iterp.hasValue = false -} - -func (iter *FunctionIterator[T]) Next(isSame NextAction) (value T, hasValue bool) { - iter.lock.Lock() - defer iter.lock.Unlock() - - // should next operation be invoked? - if iter.isEnd { - return // error from fn or cancel invoked: zero-value and hasValue false - } - - // same value when a value has previously been read - if isSame == IsSame && iter.didNext { - value = iter.value - hasValue = iter.hasValue - return - } else if !iter.didNext { - iter.didNext = true - } - // invoke fn - index := iter.index // remember out index - iter.index++ // update index for next invocation - var err error - // invokeFn unlocks while invoking fn to allow re-entrancy. - // if fn returns error, invokeFn updates: iter.isEnd iter.err iter.value iter.hasValue. - if value, hasValue, err = iter.invokeFn(index); err != nil { - return // fn error: zero-value, false - } + f := functionIterator[T]{iteratorFunction: iteratorFunction} - // update state for subsequent Same invocations - if index+1 == iter.index { // there were no intermediate invocations - iter.value = value - iter.hasValue = hasValue + var delegateAction DelegateAction[T] + return &FunctionIterator[T]{ + functionIterator: &f, + BaseIterator: *NewBaseIterator[T](f.invokeFn, &delegateAction), + // NewDelegator must be after NewBaseIterator + Delegator: *NewDelegator[T](delegateAction), } - - return // good return: value and hasValue both valid } -// invokeFn invokes iter.fn unlocked, recovers from fn panics and updates iter.err. -func (iter *FunctionIterator[T]) invokeFn(index int) (value T, hasValue bool, err error) { - - // invoke fn with unlock and panic recovery - cyclebreaker.RecoverInvocationPanic(func() { - // it is allowed for fn to invoke the iterator - iter.lock.Unlock() - defer iter.lock.Lock() - - value, err = iter.fn(index) - }, &err) - - // update error outcome - if err != nil { - iter.isEnd = true - if !errors.Is(err, cyclebreaker.ErrEndCallbacks) { - iter.err = perrors.AppendError(iter.err, err) - } - var zeroValue T - value = zeroValue - iter.value = zeroValue - iter.hasValue = false - } else { - hasValue = true +// invokeFn invokes fn recovering a possible panic +// - if cancelState == notCanceled, a new value is requested. +// Otherwise, iteration cancel is requested +// - if err is nil, value is valid and isPanic false. +// Otherwise, err is non-nil and isPanic may be set. +// value is zero-value +// - thread-safe but invocations must be serialized +func (i *functionIterator[T]) invokeFn(isCancel bool) ( + value T, + didCancel bool, + isPanic bool, + err error, +) { + defer cyclebreaker.RecoverErr(func() cyclebreaker.DA { return cyclebreaker.A() }, &err, &isPanic) + + var v T + if v, err = i.iteratorFunction(isCancel); err == nil && !isCancel { + value = v } return } - -func (iter *FunctionIterator[T]) Cancel() (err error) { - iter.lock.Lock() - defer iter.lock.Unlock() - - // if already error, or Cancel has alredy been invoked, return same result - if iter.isEnd { - err = iter.err - return - } - iter.isEnd = true - - // execute cancel - _, _, err = iter.invokeFn(FunctionIteratorCancel) - - return -} diff --git a/iters/function-iterator_test.go b/iters/function-iterator_test.go index 84ab03a0..4218a6b9 100644 --- a/iters/function-iterator_test.go +++ b/iters/function-iterator_test.go @@ -14,62 +14,66 @@ import ( ) func TestFunctionIterator(t *testing.T) { - slice := []string{"one", "two"} - //messageFnNil := "fn cannot be nil" + var values = []string{"one", "two"} var value string var hasValue bool var zeroValue string - var iter FunctionIterator[string] - fn := func(index int) (value string, err error) { - if index == FunctionIteratorCancel { - t.Log("fn cancel") - return - } - if index >= len(slice) { - t.Log("fn ErrEndCallbacks") - err = cyclebreaker.ErrEndCallbacks - _ = errors.As - return - } - value = slice[index] - t.Logf("fn value: %q", value) - return - } var err error - InitFunctionIterator(&iter, fn) + // test type that has tt.fn function that can be used with function iterator + var tt *fnIteratorTester = newFnIteratorTester(values, t) + // the iterator under test + var iterator Iterator[string] = NewFunctionIterator(tt.fn) - // Same twice + // NewFunctionIterator should return a value + if iterator == nil { + t.Error("iterator nil") + t.FailNow() + } + + // request IsSame value twice should: + // - retrieve the first value and return it + // - then return the same value again for i := 0; i <= 1; i++ { - if value, hasValue = iter.Next(IsSame); !hasValue { - t.Errorf("Same%d hasValue false", i) - } - if value != slice[0] { - t.Errorf("Same%d value %q exp %q", i, value, slice[0]) + value, hasValue = iterator.Same() + //hasValue should be true + if !hasValue { + t.Errorf("Same%d: hasValue false", i) + } + // value should be first value + if value != values[0] { + t.Errorf("Same%d value %q exp %q", i, value, values[0]) } } - // Next - if value, hasValue = iter.Next(IsNext); !hasValue { + // Next should return the second value + value, hasValue = iterator.Next() + if !hasValue { t.Errorf("Next hasValue false") } - if value != slice[1] { - t.Errorf("Next value %q exp %q", value, slice[1]) + if value != values[1] { + t.Errorf("Next value %q exp %q", value, values[1]) } - if value, hasValue = iter.Next(IsNext); hasValue { + + // Next should return no value + value, hasValue = iterator.Next() + if hasValue { t.Errorf("Next2 hasValue true") } if value != zeroValue { t.Errorf("Next2 value %q exp %q", value, zeroValue) } - if err = iter.Cancel(); err != nil { + // cancel should not return error + if err = iterator.Cancel(); err != nil { t.Errorf("Cancel err '%v'", err) } - if value, hasValue = iter.Next(IsNext); hasValue { + // Next after cancel should not return a value + value, hasValue = iterator.Next() + if hasValue { t.Errorf("Next3 hasValue true") } if value != zeroValue { @@ -77,92 +81,102 @@ func TestFunctionIterator(t *testing.T) { } } +// tests interface Iterator[string] func TestNewFunctionIterator(t *testing.T) { - slice := []string{} + var values = []string{} var zeroValue string - messageIterpNil := "cannot be nil" - fn := func(index int) (value string, err error) { - if index == FunctionIteratorCancel { - return - } - if index >= len(slice) { - err = cyclebreaker.ErrEndCallbacks - return - } - value = slice[index] - return - } + var messageIterpNil = "cannot be nil" - var iter Iterator[string] + var iterator Iterator[string] var value string var hasValue bool var err error - iter = NewFunctionIterator(fn) + // test type that has tt.fn function that can be used with function iterator + var tt = newFnIteratorTester(values, t) + iterator = NewFunctionIterator(tt.fn) - if value, hasValue = iter.Same(); hasValue { + // Same should retrieve the first value, but there isn’t one + value, hasValue = iterator.Same() + // hasValue should be false + if hasValue { t.Error("Same hasValue true") } + // value shouldbe zero-value if value != zeroValue { t.Error("Same hasValue not zeroValue") } - err = nil - cyclebreaker.RecoverInvocationPanic(func() { - NewFunctionIterator[string](nil) - }, &err) + // new with nil argument should panic + _, err = tt.invokeNewFunctionIterator(nil) if err == nil || !strings.Contains(err.Error(), messageIterpNil) { t.Errorf("InitSliceIterator incorrect panic: '%v' exp %q", err, messageIterpNil) } - if err = NewFunctionIterator(fn).Cancel(); err != nil { + // new and cancel should not return error + iterator, err = tt.invokeNewFunctionIterator(tt.fn) + _ = err + err = iterator.Cancel() + if err != nil { t.Errorf("Cancel2 err: '%v'", err) } } -func TestInitFunctionIterator(t *testing.T) { - slice := []string{"one", "two"} - messageIterpNil := "cannot be nil" - fn := func(index int) (value string, err error) { - if index == FunctionIteratorCancel { - return - } - if index >= len(slice) { - err = cyclebreaker.ErrEndCallbacks - return - } - value = slice[index] - return +// fnIteratorTester is a fixture for testing Iterator[T] implementations +type fnIteratorTester struct { + index int // index is current index in slice + values []string // values are ethe values provided during iteration + t *testing.T // testing instance +} + +func newFnIteratorTester(values []string, t *testing.T) (tt *fnIteratorTester) { + return &fnIteratorTester{ + values: values, + t: t, } +} - var iter FunctionIterator[string] - var value string - var hasValue bool - var iterpNil *FunctionIterator[string] - var err error +var _ IteratorFunction[string] = (&fnIteratorTester{}).fn - InitFunctionIterator(&iter, fn) +// tt.fn is a function that can be used with function iterator +func (tt *fnIteratorTester) fn(isCancel bool) (value string, err error) { + t := tt.t - if value, hasValue = iter.Next(IsSame); !hasValue { - t.Error("Same hasValue false") - } - if value != slice[0] { - t.Errorf("Same value %q exp %q", value, slice[0]) + // on cancel request + if isCancel { + t.Log("fn cancel") + return } - err = nil - cyclebreaker.RecoverInvocationPanic(func() { - InitFunctionIterator(iterpNil, fn) - }, &err) - if err == nil || !strings.Contains(err.Error(), messageIterpNil) { - t.Errorf("InitSliceIterator incorrect panic: '%v' exp %q", err, messageIterpNil) - } + // index should be a sequence 0… - err = nil - cyclebreaker.RecoverInvocationPanic(func() { - InitFunctionIterator(&iter, nil) - }, &err) - if err == nil || !strings.Contains(err.Error(), messageIterpNil) { - t.Errorf("InitSliceIterator incorrect panic: '%v' exp %q", err, messageIterpNil) + // index beyond number of values + if tt.index >= len(tt.values) { + t.Log("fn ErrEndCallbacks") + err = cyclebreaker.ErrEndCallbacks + _ = errors.As + return } + + // return value + value = tt.values[tt.index] + tt.index++ + t.Logf("fn value: %q", value) + + return +} +func (tt *fnIteratorTester) invokeNewFunctionIterator(fn IteratorFunction[string]) ( + iterator Iterator[string], + err error, +) { + defer func() { + v := recover() + if v == nil { + return + } + err = v.(error) + }() + + iterator = NewFunctionIterator[string](fn) + return } diff --git a/iters/if-iterator-provider.go b/iters/if-iterator-provider.go new file mode 100644 index 00000000..e35de4c6 --- /dev/null +++ b/iters/if-iterator-provider.go @@ -0,0 +1,54 @@ +/* +© 2022-present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +All rights reserved +*/ + +package iters + +import "golang.org/x/exp/constraints" + +// when the iterator function receives this value, it means cancel +const FunctionIteratorCancel int = -1 + +// IteratorFunction is the signature used by NewFunctionIterator +// - if isCancel true, it means this is the last invocation of IteratorFunction and +// IteratorFunction should release any resources. +// Any returned value is not used +// - IteratorFunction signals end of values by returning parl.ErrEndCallbacks. +// Any returned value is not used +// - if IteratorFunction returns error, it will not be invoked again. +// Any returned value is not used +// - IteratorFunction must be thread-safe +// - IteratorFunction is invoked by at most one thread at a time +type IteratorFunction[T any] func(isCancel bool) (value T, err error) + +// ConverterFunction is the signature used by NewConverterIterator +// - ConverterFunction receives a key and returns the corresponding value. +// - if isCancel true, it means this is the last invocation of ConverterFunction and +// ConverterFunction should release any resources. +// Any returned value is not used +// - ConverterFunction signals end of values by returning parl.ErrEndCallbacks. +// Any returned value is not used +// - if ConverterFunction returns error, it will not be invoked again. +// Any returned value is not used +// - ConverterFunction must be thread-safe +// - ConverterFunction is invoked by at most one thread at a time +type ConverterFunction[K constraints.Ordered, V any] func(key K, isCancel bool) (value V, err error) + +// InvokeFunc is the signature used by NewBaseIterator +// - isCancel is a request to cancel iteration. No more invocations +// will occur. Iterator should release resources +// - value is valid if err is nil and isCancel was false and didCancel is false. +// otherwise, value is ignored +// - if err is non-nil an error occurred. +// parl.ErrEndCallbacks allows InvokeFunc to signal end of iteration values. +// parl.ErrEndCallbacks error is ignored. +// - if isCancel was false and didCancel is true, IncokeFunc took the +// initiative to cancel the iteration. No more invocations will occur +// - isPanic indicates that err is the result of a panic +type InvokeFunc[T any] func(isCancel bool) ( + value T, + didCancel bool, + isPanic bool, + err error, +) diff --git a/iters/if-iterator.go b/iters/if-iterator.go index f63e990d..7359436a 100644 --- a/iters/if-iterator.go +++ b/iters/if-iterator.go @@ -21,28 +21,31 @@ package iters // } // if err = iterator.Cancel(); … type Iterator[T any] interface { - // Next advances to next item and returns it. - // if the next item does exist, value is valid and hasValue is true. - // if no next item exists, value is the data type zero-value and hasValue is false. + // Next advances to next item and returns it + // - if the next item does exist, value is valid and hasValue is true + // - if no next item exists, value is the data type zero-value and hasValue is false Next() (value T, hasValue bool) - // HasNext advances to next item and returns hasValue true if this next item does exists. + // HasNext advances to next item and returns hasValue true if this next item does exists HasNext() (hasValue bool) // NextValue advances to next item and returns it. - // If no next value exists, the data type zero-value is returned. + // - If no next value exists, the data type zero-value is returned NextValue() (value T) // Same returns the same value again. - // If a value does exist, it is returned in value and hasValue is true. - // If a value does not exist, the data type zero-value is returned and hasValue is false. - // If Next, FindNext or HasNext have not been invoked, Same first advances to the first item. + // - If a value does exist, it is returned in value and hasValue is true + // - If a value does not exist, the data type zero-value is returned and hasValue is false + // - If Next, FindNext or HasNext have not been invoked, Same first advances to the first item Same() (value T, hasValue bool) // Has returns true if Same() or SameValue will return items. - // If Next, FindNext or HasNext have not been invoked, Has first advances to the first item. + // - If Next, FindNext or HasNext have not been invoked, + // Has first advances to the first item Has() (hasValue bool) - // SameValue returns the same value again. - // If a value does not exist, the data type zero-value is returned. - // If Next, FindNext or HasNext have not been invoked, SameValue first advances to the first item. + // SameValue returns the same value again + // - If a value does not exist, the data type zero-value is returned + // - If Next, FindNext or HasNext have not been invoked, + // SameValue first advances to the first item SameValue() (value T) - // Cancel release resources for this iterator. - // Not every iterator requires a Cancel invocation. + // Cancel release resources for this iterator + // - returns the first error that occurred during iteration if any + // - Not every iterator requires a Cancel invocation Cancel() (err error) } diff --git a/iters/next-action.go b/iters/next-action.go index 9d5cfbe7..6a89ca96 100644 --- a/iters/next-action.go +++ b/iters/next-action.go @@ -5,23 +5,14 @@ All rights reserved package iters -import "strconv" +import "fmt" -const ( - IsSame NextAction = 0 // IsSame indicates to Delegate.Next that this is a Same-type incovation - IsNext NextAction = 1 // IsNext indicates to Delegate.Next that this is a Next-type incovation -) - -// NextAction is a unique named type that indicates whether -// the next or the same value again is sought by Delegate.Next -type NextAction uint8 - -func (na NextAction) String() (s string) { +func (a NextAction) String() (s string) { var ok bool - if s, ok = nextActionSet[na]; ok { + if s, ok = nextActionSet[a]; ok { return } - s = "?\x27" + strconv.Itoa(int(na)) + "\x27" + s = fmt.Sprintf("?“%d”", a) return } diff --git a/iters/slice-iterator.go b/iters/slice-iterator.go index bb000d13..a95178b5 100644 --- a/iters/slice-iterator.go +++ b/iters/slice-iterator.go @@ -7,78 +7,97 @@ package iters import ( "sync" - - "github.com/haraldrudell/parl/perrors" + "sync/atomic" ) // SliceIterator traverses a slice container. thread-safe type SliceIterator[T any] struct { - lock sync.Mutex - didNext bool // indicates whether any value has been sought - hasValue bool // indicates whether index has been verified to be valid - index int // index in slice, 0…len(slice) - slice []T + slice []T // the slice providing values + + // isEnd is fast outside-lock check for no values available + isEnd atomic.Bool + + // lock serializes Next and Cancel invocations + lock sync.Mutex + // didNext indicates that the first value was sought + // - behind lock + didNext bool + // hasValue indicates that slice[index] is the current value + // - behind lock + hasValue bool + // index in slice, 0…len(slice) + // - behind lock + index int + + // Delegator implements the value methods required by the [Iterator] interface + // - Next HasNext NextValue + // Same Has SameValue + // - the delegate provides DelegateAction[T] function + Delegator[T] } // NewSliceIterator returns an empty iterator of values type T. // sliceIterator is thread-safe. func NewSliceIterator[T any](slice []T) (iterator Iterator[T]) { - return &Delegator[T]{Delegate: &SliceIterator[T]{slice: slice}} + i := SliceIterator[T]{slice: slice} + i.Delegator = *NewDelegator(i.delegateAction) + return &i } -// InitSliceIterator initializes a SliceIterator struct. -// sliceIterator is thread-safe. -func InitSliceIterator[T any](iterp *SliceIterator[T], slice []T) { - if iterp == nil { - panic(perrors.NewPF("iterator cannot be nil")) +// delegateAction finds the next or the same value. Thread-safe +// - isSame == IsSame means first or same value should be returned +// - value is the sought value or the T type’s zero-value if no value exists +// - hasValue true means value was assigned a valid T value +func (i *SliceIterator[T]) delegateAction(isSame NextAction) (value T, hasValue bool) { + + if i.isEnd.Load() { + return // no more values return } - iterp.lock = sync.Mutex{} - iterp.didNext = false - iterp.hasValue = false - iterp.index = 0 - iterp.slice = slice -} -func (iter *SliceIterator[T]) Next(isSame NextAction) (value T, hasValue bool) { - iter.lock.Lock() - defer iter.lock.Unlock() + i.lock.Lock() + defer i.lock.Unlock() - // if next operation has not completed, we do not know if a value exist, - // and next operation must be completed. - // if next has completed and we seek the same value, next operation should not be done. - if !iter.didNext || isSame != IsSame { + if i.isEnd.Load() { + return // no more values return + } + + // for IsSame operation the first value must be sought + // - therefore, if the first value has not been sought, seek it now or + // - if not IsSame operation, advance to the next value + if !i.didNext || isSame != IsSame { + + // note that first value has been sought + if !i.didNext { + i.didNext = true + } // find slice index to use - if iter.hasValue { - // if a value has been found and is valid, advance index. - // the final value for iter.index is len(iter.slice) - iter.index++ + // - if a value was found, advance index + // - final i.index value is len(i.slice) + if i.hasValue { + i.index++ } // check if the new index is within available slice values - // when iter.index has reached len(iter.slice), iter.hasValue is always false. - // when hasValue is false, iter.index will no longer be incremented. - iter.hasValue = iter.index < len(iter.slice) - - // indicate that iter.hasValue is now valid - if !iter.didNext { - iter.didNext = true - } + // - when i.index has reached len(i.slice), i.hasValue is always false + // - when hasValue is false, i.index will no longer be incremented + i.hasValue = i.index < len(i.slice) } - // get the value if it is valid, otherwise zero-value - if hasValue = iter.hasValue; hasValue { - value = iter.slice[iter.index] + // update hasValue and value + // - get the value if it is valid, otherwise zero-value + if hasValue = i.hasValue; hasValue { + value = i.slice[i.index] + } else { + i.isEnd.CompareAndSwap(false, true) } return // value and hasValue indicates availability } -func (iter *SliceIterator[T]) Cancel() (err error) { - iter.lock.Lock() - defer iter.lock.Unlock() - - iter.hasValue = false // invalidate iter.value - iter.slice = nil // prevent any next operation +// Cancel release resources for this iterator. Thread-safe +// - not every iterator requires a Cancel invocation +func (i *SliceIterator[T]) Cancel() (err error) { + i.isEnd.CompareAndSwap(false, true) return } diff --git a/iters/slice-iterator_test.go b/iters/slice-iterator_test.go index e31d2047..b4a4a530 100644 --- a/iters/slice-iterator_test.go +++ b/iters/slice-iterator_test.go @@ -6,96 +6,74 @@ All rights reserved package iters import ( - "strings" + "slices" "testing" - - "github.com/haraldrudell/parl/internal/cyclebreaker" - "golang.org/x/exp/slices" ) func TestSliceIterator(t *testing.T) { - slice := []string{"one", "two"} - var zeroValue string + var values = []string{"one", "two"} var err error - var iter SliceIterator[string] var value string var hasValue bool + var zeroValue string - InitSliceIterator(&iter, slices.Clone(slice)) + var iterator Iterator[string] = NewSliceIterator(slices.Clone(values)) - // Same twice + // request IsSame value twice should: + // - retrieve the first value and return it + // - then return the same value again for i := 0; i <= 1; i++ { - if value, hasValue = iter.Next(IsSame); !hasValue { + value, hasValue = iterator.Same() + + //hasValue should be true + if !hasValue { t.Errorf("Same%d hasValue false", i) } - if value != slice[0] { - t.Errorf("Same%d value %q exp %q", i, value, slice[0]) + // value should be first value + if value != values[0] { + t.Errorf("Same%d value %q exp %q", i, value, values[0]) } } - // Next - if value, hasValue = iter.Next(IsNext); !hasValue { + // Next should return the second value + value, hasValue = iterator.Next() + if !hasValue { t.Errorf("Next hasValue false") } - if value != slice[1] { - t.Errorf("Next value %q exp %q", value, slice[1]) + if value != values[1] { + t.Errorf("Next value %q exp %q", value, values[1]) } - if value, hasValue = iter.Next(IsNext); hasValue { + + // Next should return no value + value, hasValue = iterator.Next() + if hasValue { t.Errorf("Next2 hasValue true") } if value != zeroValue { t.Errorf("Next2 value %q exp %q", value, zeroValue) } - if err = iter.Cancel(); err != nil { + // cancel should not return error + if err = iterator.Cancel(); err != nil { t.Errorf("Cancel err '%v'", err) } } func TestNewSliceIterator(t *testing.T) { - slice := []string{} - var zeroValue string + var values = []string{} - var iter Iterator[string] var value string var hasValue bool + var zeroValue string - iter = NewSliceIterator(slices.Clone(slice)) + var iterator Iterator[string] = NewSliceIterator(slices.Clone(values)) - if value, hasValue = iter.Same(); hasValue { + if value, hasValue = iterator.Same(); hasValue { t.Error("Same hasValue true") } if value != zeroValue { t.Error("Same hasValue not zeroValue") } } - -func TestInitSliceIterator(t *testing.T) { - slice := []string{"one"} - messageIterpNil := "cannot be nil" - - var iter SliceIterator[string] - var value string - var hasValue bool - var iterpNil *SliceIterator[string] - var err error - - iter = SliceIterator[string]{} - InitSliceIterator(&iter, slices.Clone(slice)) - - if value, hasValue = iter.Next(IsSame); !hasValue { - t.Error("Same hasValue false") - } - if value != slice[0] { - t.Errorf("Same value %q exp %q", value, slice[0]) - } - - cyclebreaker.RecoverInvocationPanic(func() { - InitSliceIterator(iterpNil, slice) - }, &err) - if err == nil || !strings.Contains(err.Error(), messageIterpNil) { - t.Errorf("InitSliceIterator incorrect panic: '%v' exp %q", err, messageIterpNil) - } -} diff --git a/iters/slice-pointer-iterator.go b/iters/slice-pointer-iterator.go new file mode 100644 index 00000000..c8f07cd2 --- /dev/null +++ b/iters/slice-pointer-iterator.go @@ -0,0 +1,110 @@ +/* +© 2023-present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +All rights reserved +*/ + +package iters + +import ( + "sync" + "sync/atomic" +) + +// SlicePointerIterator traverses a slice container using pointers to value. thread-safe. +// - the difference is that: +// - instead of copying a value from the slice, +// - a pointer to the slice value is returned +type SlicePointerIterator[T any] struct { + slice []T // the slice providing values + + // isEnd is fast outside-lock check for no values available + isEnd atomic.Bool + + // lock serializes Next and Cancel invocations + lock sync.Mutex + // didNext indicates that the first value was sought + // - behind lock + didNext bool + // hasValue indicates that slice[index] is the current value + // - behind lock + hasValue bool + // index in slice, 0…len(slice) + // - behind lock + index int + + // Delegator implements the value methods required by the [Iterator] interface + // - Next HasNext NextValue + // Same Has SameValue + // - the delegate provides DelegateAction[T] function + Delegator[*T] +} + +// NewSlicePointerIterator returns an iterator of pointers to T +// - the difference is that: +// - instead of copying a value from the slice, +// - a pointer to the slice value is returned +// - the returned [Iterator] value cannot be copied, the pointer value +// must be used +func NewSlicePointerIterator[T any](slice []T) (iterator Iterator[*T]) { + i := SlicePointerIterator[T]{slice: slice} + i.Delegator = *NewDelegator(i.delegateAction) + return &i +} + +// Next finds the next or the same value. Thread-safe +// - isSame true means first or same value should be returned +// - value is the sought value or the T type’s zero-value if no value exists +// - hasValue true means value was assigned a valid T value +func (i *SlicePointerIterator[T]) delegateAction(isSame NextAction) (value *T, hasValue bool) { + + if i.isEnd.Load() { + return // no more values return + } + + i.lock.Lock() + defer i.lock.Unlock() + + if i.isEnd.Load() { + return // no more values return + } + + // for IsSame operation the first value must be sought + // - therefore, if the first value has not been sought, seek it now or + // - if not IsSame operation, advance to the next value + if !i.didNext || isSame != IsSame { + + // note that first value has been sought + if !i.didNext { + i.didNext = true + } + + // find slice index to use + // - if a value was found, advance index + // - final i.index value is len(i.slice) + if i.hasValue { + i.index++ + } + + // check if the new index is within available slice values + // - when i.index has reached len(i.slice), i.hasValue is always false + // - when hasValue is false, i.index will no longer be incremented + i.hasValue = i.index < len(i.slice) + } + + // update hasValue and value + // - get the value if it is valid, otherwise zero-value + if hasValue = i.hasValue; hasValue { + value = &i.slice[i.index] + } else { + i.isEnd.CompareAndSwap(false, true) + } + + return // value and hasValue indicates availability +} + +// Cancel release resources for this iterator. Thread-safe +// - not every iterator requires a Cancel invocation +func (i *SlicePointerIterator[T]) Cancel() (err error) { + i.isEnd.CompareAndSwap(false, true) + return +} diff --git a/iters/slice-pointer-iterator_test.go b/iters/slice-pointer-iterator_test.go new file mode 100644 index 00000000..2aa53231 --- /dev/null +++ b/iters/slice-pointer-iterator_test.go @@ -0,0 +1,69 @@ +/* +© 2023-present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +All rights reserved +*/ + +package iters + +import ( + "slices" + "testing" +) + +// p converts a string pointer to a string value +func p(value *string) (s string) { + if value != nil { + return *value + } + return "nil" +} + +func TestNewSlicePointerIterator(t *testing.T) { + var values = []string{"one", "two"} + + var err error + var value *string + var hasValue bool + var zeroValue *string + + var iterator Iterator[*string] = NewSlicePointerIterator(slices.Clone(values)) + + // request IsSame value twice should: + // - retrieve the first value and return it + // - then return the same value again + for i := 0; i <= 1; i++ { + value, hasValue = iterator.Same() + + //hasValue should be true + if !hasValue { + t.Errorf("Same%d hasValue false", i) + } + // value should be first value + if value == nil || *value != values[0] { + t.Errorf("Same%d value %q exp %q", i, p(value), values[0]) + } + } + + // Next should return the second value + value, hasValue = iterator.Next() + if !hasValue { + t.Errorf("Next hasValue false") + } + if value == nil || *value != values[1] { + t.Errorf("Next value %q exp %q", p(value), values[1]) + } + + // Next should return no value + value, hasValue = iterator.Next() + if hasValue { + t.Errorf("Next2 hasValue true") + } + if value != zeroValue { + t.Errorf("Next2 value %q exp %q", p(value), p(zeroValue)) + } + + // cancel should not return error + if err = iterator.Cancel(); err != nil { + t.Errorf("Cancel err '%v'", err) + } +} diff --git a/iters/slicep-iterator.go b/iters/slicep-iterator.go deleted file mode 100644 index 73a29f5e..00000000 --- a/iters/slicep-iterator.go +++ /dev/null @@ -1,85 +0,0 @@ -/* -© 2023-present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) -All rights reserved -*/ - -// SlicePIterator traverses a slice container using pointers to value. thread-safe. -package iters - -import ( - "sync" - - "github.com/haraldrudell/parl/perrors" -) - -// SlicePIterator traverses a slice container using pointers to value. thread-safe. -type SlicePIterator[T any] struct { - lock sync.Mutex - didNext bool // indicates whether any value has been sought - hasValue bool // indicates whether index has been verified to be valid - index int // index in slice, 0…len(slice) - slice []T -} - -// NewSlicePIterator returns an iterator of pointers to T. -// SlicePIterator is thread-safe. -func NewSlicePIterator[T any](slice []T) (iterator Iterator[*T]) { - return &Delegator[*T]{Delegate: &SlicePIterator[T]{slice: slice}} -} - -// InitSliceIterator initializes a SliceIterator struct. -// sliceIterator is thread-safe. -func InitSlicePIterator[T any](iterp *SliceIterator[T], slice []T) { - if iterp == nil { - panic(perrors.NewPF("iterator cannot be nil")) - } - iterp.lock = sync.Mutex{} - iterp.didNext = false - iterp.hasValue = false - iterp.index = 0 - iterp.slice = slice -} - -func (iter *SlicePIterator[T]) Next(isSame NextAction) (value *T, hasValue bool) { - iter.lock.Lock() - defer iter.lock.Unlock() - - // if next operation has not completed, we do not know if a value exist, - // and next operation must be completed. - // if next has completed and we seek the same value, next operation should not be done. - if !iter.didNext || isSame != IsSame { - - // find slice index to use - if iter.hasValue { - // if a value has been found and is valid, advance index. - // the final value for iter.index is len(iter.slice) - iter.index++ - } - - // check if the new index is within available slice values - // when iter.index has reached len(iter.slice), iter.hasValue is always false. - // when hasValue is false, iter.index will no longer be incremented. - iter.hasValue = iter.index < len(iter.slice) - - // indicate that iter.hasValue is now valid - if !iter.didNext { - iter.didNext = true - } - } - - // get the value if it is valid, otherwise zero-value - if hasValue = iter.hasValue; hasValue { - value = &iter.slice[iter.index] - } - - return // value and hasValue indicates availability -} - -func (iter *SlicePIterator[T]) Cancel() (err error) { - iter.lock.Lock() - defer iter.lock.Unlock() - - iter.hasValue = false // invalidate iter.value - iter.slice = nil // prevent any next operation - return -} diff --git a/mains/go.mod b/mains/go.mod index af16ed64..49c18c57 100644 --- a/mains/go.mod +++ b/mains/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.3 replace github.com/haraldrudell/parl => ../../parl require ( - github.com/haraldrudell/parl v0.4.115 + github.com/haraldrudell/parl v0.4.116 golang.org/x/sys v0.13.0 ) diff --git a/mains/keystrokes.go b/mains/keystrokes.go index 29398c00..b9048c41 100644 --- a/mains/keystrokes.go +++ b/mains/keystrokes.go @@ -54,7 +54,7 @@ func (k *Keystrokes) CloseNow(errp *error) { func keystrokesThread(stdin *parl.NBChan[string]) { var err error defer parl.Debug("keystrokes.scannerThread exiting: err: %s", perrors.Short(err)) - defer parl.Recover(parl.Annotation(), &err, parl.Infallible) + defer parl.Recover("", &err, parl.Infallible) var scanner = bufio.NewScanner(os.Stdin) parl.Debug("keystrokes.scannerThread scanning: stdin.Ch: 0x%x", stdin.Ch()) diff --git a/moderator.go b/moderator.go index d9363289..4af0173c 100644 --- a/moderator.go +++ b/moderator.go @@ -5,10 +5,6 @@ ISC License package parl -import ( - "context" -) - /* Moderator invokes functions at a limited level of parallelism. It is a ticketing system @@ -22,7 +18,7 @@ It is a ticketing system */ type Moderator struct { moderatorCore - ctx context.Context + //ctx context.Context } // NewModerator creates a cancelable Moderator used to limit parallelism diff --git a/nb-chan-close.go b/nb-chan-close.go index bfa43db4..cb7b2882 100644 --- a/nb-chan-close.go +++ b/nb-chan-close.go @@ -12,7 +12,7 @@ import ( ) const ( - dataWaiterAfterClose = true +// dataWaiterAfterClose = true ) // Close orders the channel to close once pending sends complete. diff --git a/nb-chan-thread.go b/nb-chan-thread.go index 0f794d80..2ce4e3e0 100644 --- a/nb-chan-thread.go +++ b/nb-chan-thread.go @@ -16,7 +16,7 @@ func (n *NBChan[T]) sendThread(value T) { var endCh = *n.threadWait.Load() defer close(endCh) defer n.sendThreadDeferredClose() - defer Recover(Annotation(), nil, n.sendThreadOnError) + defer Recover("", nil, n.sendThreadOnError) for { // send value loop diff --git a/nb-chan.go b/nb-chan.go index f7e74ac3..3c80e2a0 100644 --- a/nb-chan.go +++ b/nb-chan.go @@ -77,7 +77,7 @@ const ( // func thread(errCh *parl.NBChan[error]) { // defer errCh.Close() // non-blocking close effective on send complete // var err error -// defer parl.Recover(parl.Annotation(), &err, errCh.AddErrorProc) +// defer parl.Recover(parl."", &err, errCh.AddErrorProc) // errCh.Ch() <- err // non-blocking // if err = someFunc(); err != nil { // err = perrors.Errorf("someFunc: %w", err) diff --git a/on-cancel.go b/on-cancel.go index d8103317..0609351b 100644 --- a/on-cancel.go +++ b/on-cancel.go @@ -14,7 +14,7 @@ func OnCancel(fn func(), ctx context.Context) { } func onCancelThread(fn func(), done <-chan struct{}) { - Recover(Annotation(), nil, Infallible) + Recover("", nil, Infallible) <-done fn() } diff --git a/once-waiter.go b/once-waiter.go index c45bce66..38845c56 100644 --- a/once-waiter.go +++ b/once-waiter.go @@ -76,7 +76,7 @@ func (ow *OnceWaiter) Cancel() { } func onceWaiterSender(done <-chan struct{}, ch chan<- struct{}) { - Recover(Annotation(), nil, Infallible) // panic prints to stderr + Recover("", nil, Infallible) // panic prints to stderr <-done ch <- struct{}{} diff --git a/panic-to-err.go b/panic-to-err.go new file mode 100644 index 00000000..b6d355d3 --- /dev/null +++ b/panic-to-err.go @@ -0,0 +1,55 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package parl + +import "github.com/haraldrudell/parl/perrors" + +// PanicToErr recovers active panic, aggregating errors in errp +// - errp cannot be nil +// - if isPanic is non-nil and active panic, it is set to true +// +// sample error message, including message in the panic value and the code line +// causing the panic: +// +// recover from panic: message: “runtime error: invalid memory address or nil pointer dereference” at parl.panicFunction()-panic-to-err_test.go:96 +// +// Usage: +// +// func someFunc() (isPanic bool, err error) { +// defer parl.PanicToErr(&err, &isPanic) +// +// func someGoroutine(g parl.Go) { +// var err error +// defer g.Register().Done(&err) +// defer parl.PanicToErr(&err) +func PanicToErr(errp *error, isPanic ...*bool) { + if errp == nil { + panic(perrors.NewPF("errp cannot be nil")) + } + + // if no panic, noop + // - recover invocation must be directly in the PanicToErr function + var panicValue = recover() + if panicValue == nil { + return // no panic active return + } + + // set isPanic if non-nil + if len(isPanic) > 0 { + if isPanicp := isPanic[0]; isPanicp != nil { + *isPanicp = true + } + } + + // append panic to *errp + // - for panic detection to work there needs to be a stack frame after runtime + // - because PanicToErr is invoked directly by the runtime, eg. pruntime.gopanic, + // the PanicToErr stack frame is included, therefore 0 argument + // to processRecover + *errp = perrors.AppendError(*errp, + processRecover("recover from panic: message:", panicValue, 0), + ) +} diff --git a/panic-to-err_test.go b/panic-to-err_test.go new file mode 100644 index 00000000..20ed16b4 --- /dev/null +++ b/panic-to-err_test.go @@ -0,0 +1,105 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package parl + +import ( + "fmt" + "strings" + "testing" + + "github.com/haraldrudell/parl/errorglue" + "github.com/haraldrudell/parl/perrors" + "github.com/haraldrudell/parl/pruntime" +) + +func TestPanicToErr(t *testing.T) { + // "runtime error: invalid memory address or nil pointer dereference" + var panicMessage = func() (message string) { + defer func() { message = recover().(error).Error() }() + _ = *(*int)(nil) // causes nil pointer dereference panic + return + }() + // “runtime error: invalid memory address or nil pointer dereference” + var expMessage = fmt.Sprintf("“%s”", panicMessage) + + var panicLine *pruntime.CodeLocation + var err error + var stack pruntime.StackSlice + var errorShort string + + // get [parl.RecoverErr] values from recovering a panic + panicLine, err = panicFunction() + + // there should be an error + if err == nil { + t.Error("expected error missing") + t.FailNow() + } + stack = errorglue.GetStackTrace(err) + errorShort = perrors.Short(err) + + // panicLine: “ + // File: "/opt/sw/parl/recover-err_test.go" + // Line: 24 + // FuncName: "github.com/haraldrudell/parl.panicFunction" + //” + t.Logf("panicLine: “%s”", panicLine.Dump()) + + // stack trace: + // runtime.gopanic + // /opt/homebrew/Cellar/go/1.21.3/libexec/src/runtime/panic.go:914 + // runtime.panicmem + // /opt/homebrew/Cellar/go/1.21.3/libexec/src/runtime/panic.go:261 + // runtime.sigpanic + // /opt/homebrew/Cellar/go/1.21.3/libexec/src/runtime/signal_unix.go:861 + // github.com/haraldrudell/parl.panicFunction + // /opt/sw/parl/recover-err_test.go:23 + // github.com/haraldrudell/parl.TestRecoverErr + // /opt/sw/parl/recover-err_test.go:32 + // testing.tRunner + // /opt/homebrew/Cellar/go/1.21.3/libexec/src/testing/testing.go:1595 + // runtime.goexit + // /opt/homebrew/Cellar/go/1.21.3/libexec/src/runtime/asm_arm64.s:1197 + // /opt/sw/parl/recover-err_test.go:43: bad error message + t.Logf("stack trace: %s", stack) + + // error: + // Recover from panic in runtime.gopanic:: panic: + // 'runtime error: invalid memory address or nil pointer dereference' + // at runtime.gopanic()-panic.go:914 + t.Logf("error: %s", errorShort) + + // perrors.Short should detect the exact location of the panic + var panicLineShort = panicLine.Short() + if !strings.HasSuffix(errorShort, panicLineShort) { + t.Errorf("perrors.Short does not end with exact panic location:\n%s\n%s", + errorShort, + panicLineShort, + ) + } + + // perrors.Short should contain the message + if !strings.Contains(errorShort, expMessage) { + t.Errorf("perrors.Short does not contain expected error message::\n%s\n%s", + errorShort, + expMessage, + ) + } +} + +// panicFunction recovers a panic using [parl.RecoverErr] +// - panicLine is the exact code line of the panic +// - err is the error value produced by [parl.RecoverErr] +func panicFunction() (panicLine *pruntime.CodeLocation, err error) { + defer PanicToErr(&err) + + // get exact code line and generate a nil pointer dereference panic + if panicLine = pruntime.NewCodeLocation(0); *(*int)(nil) != 0 { + _ = 1 + } + + return +} diff --git a/parl.go b/parl.go index c94effff..1fe314a3 100644 --- a/parl.go +++ b/parl.go @@ -60,7 +60,7 @@ mains.Recover is similar for the process. func thread(errCh *parl.NBChan[error]) { // real-time non-blocking error channel defer errCh.Close() // non-blocking close effective on send complete var err error - defer parl.Recover2(parl.Annotation(), &err, errCh.Send) + defer parl.Recover2("", &err, errCh.Send) errCh.Ch() <- err // non-blocking if err = someFunc(); err != nil { err = perrors.Errorf("someFunc: %w", err) // labels and attaches a stack diff --git a/parli/if-map.go b/parli/if-map.go index ef7dfb04..1b612171 100644 --- a/parli/if-map.go +++ b/parli/if-map.go @@ -5,92 +5,11 @@ ISC License package parli -import "golang.org/x/exp/constraints" - const ( MapDeleteWithZeroValue = true MapClearUsingRange = true ) -// pmaps.AggregatingPriorityQueue uses cached priority obtained from -// Aggregators that operates on the values outside of the AggregatingPriorityQueue. -// - the Update method reprioritizes an updated value element -type AggregatingPriorityQueue[V any, P constraints.Ordered] interface { - // Get retrieves a possible value container associated with valuep - Get(valuep *V) (aggregator Aggregator[V, P], ok bool) - // Put stores a new value container associated with valuep - // - the valuep is asusmed to not have a node in the queue - Put(valuep *V, aggregator Aggregator[V, P]) - // Update re-prioritizes a value - Update(valuep *V) - // Clear empties the priority queue. The hashmap is left intact. - Clear() - // List returns the first n or default all values by pirority - List(n ...int) (aggregatorQueue []AggregatePriority[V, P]) -} - -// PriorityQueue is a pointer-identity-to-value map of updatable values traversable by rank. -// - PriorityQueue operates directly on value by caching priority from the pritority function. -// - the AddOrUpdate method reprioritizes an updated value element -// - V is a value reference composite type that is comparable, ie. not slice map function. -// Preferrably, V is interface or pointer to struct type. -// - P is an ordered type such as Integer Floating-Point or string, used to rank the V values -// - values are added or updated using AddOrUpdate method distinguished by -// (computer science) identity -// - if the same comparable value V is added again, that value is re-ranked -// - priority P is computed from a value V using the priorityFunc function. -// The piority function may be examining field values of a struct -// - values can have the same rank. If they do, equal rank is provided in insertion order -// - pmaps.NewPriorityQueue[V any, P constraints.Ordered] -// - pmaps.NewRankingThreadSafe[V comparable, R constraints.Ordered]( -// ranker func(value *V) (rank R))) -type PriorityQueue[V any, P constraints.Ordered] interface { - // AddOrUpdate adds a new value to the prioirty queue or updates the priority of a value - // that has changed. - AddOrUpdate(value *V) - // List returns the first n or default all values by priority - List(n ...int) (valueQueue []*V) -} - -// AggregatePriority caches the priority value from an aggregator for priority. -// - V is the value type used as a pointer -// - P is the priority type descending order, ie. Integer Floating-Point string -type AggregatePriority[V any, P constraints.Ordered] interface { - // Aggregator returns the aggregator associated with this AggregatePriority - Aggregator() (aggregator Aggregator[V, P]) - // Update caches the current priority from the aggregator - Update() - // Priority returns the effective cached priority - // - Priority is used by consumers of the AggregatingPriorityQueue - CachedPriority() (priority P) - // Index indicates insertion order - // - used for ordering elements of equal priority - Index() (index int) -} - -// Aggregator aggregates, snapshots and assigns priority to an associated value. -// - V is the value type used as a pointer -// - V may be a thread-safe object whose values change in real-time -// - P is the priority type descending order, ie. Integer Floating-Point string -type Aggregator[V any, P constraints.Ordered] interface { - // Value returns the value object this Aggregator is associated with - // - the Value method is used by consumers of the AggregatingPriorityQueue - Value() (valuep *V) - // Aggregate aggregates and snapshots data values from the value object. - // - Aggregate is invoked outside of AggregatingPriorityQueue - Aggregate() - // Priority returns the current priority for the associated value - // - this priority is cached by AggregatePriority - Priority() (priority P) -} - -// AssignedPriority contains the assigned priority for a priority-queue element -// - V is the element value type whose pointer-value provides identity -// - P is the priority, a descending-ordered type: Integer Floating-Point string -type AssignedPriority[V any, P constraints.Ordered] interface { - SetPriority(priority P) -} - // ThreadSafeMap is a one-liner thread-safe mapping. // - pmaps.NewRWMap[K comparable, V any]() type ThreadSafeMap[K comparable, V any] interface { @@ -140,11 +59,7 @@ type ThreadSafeMap[K comparable, V any] interface { Length() (length int) // Clone returns a shallow clone of the map Clone() (clone ThreadSafeMap[K, V]) - // Swap replaces the map with otherMap and returns the current map in previousMap - // - if otherMap is not initialized RWMap, no swap takes place and previousMap is nil - // - Swap is an atomic, thread-safe operation - Swap(otherMap ThreadSafeMap[K, V]) (previousMap ThreadSafeMap[K, V]) // List provides the mapped values, undefined ordering // - O(n) - List() (list []V) + List(n ...int) (list []V) } diff --git a/periodically.go b/periodically.go index 5f67da04..cdee4d2f 100644 --- a/periodically.go +++ b/periodically.go @@ -41,7 +41,7 @@ func (p *Periodically) Wait() { func (p *Periodically) doThread() { defer p.wg.Done() - defer Recover(Annotation(), nil, Infallible) + defer Recover("", nil, Infallible) ticker := time.NewTicker(p.period) defer ticker.Stop() @@ -58,7 +58,7 @@ func (p *Periodically) doThread() { } func (p *Periodically) doFn(t time.Time) { - defer Recover(Annotation(), nil, Infallible) + defer Recover("", nil, Infallible) p.fn(t) } diff --git a/perrors/is-panic.go b/perrors/is-panic.go index d4d05d19..bb8c5e33 100644 --- a/perrors/is-panic.go +++ b/perrors/is-panic.go @@ -11,17 +11,16 @@ import ( ) // IsPanic determines if err is the result of a panic. Thread-safe -// The err is an error value that may have an error chain and needs to have a stack trace -// captured in deferred recovery code arresting the panic. -// - isPanic indicates if the error stack was captured during a panic +// - isPanic is true if a panic was detected in the inner-most stack trace of err’s error chain +// - err must have stack trace from [perrors.ErrorfPF] [perrors.Stackn] or similar function // - stack[recoveryIndex] is the code line of the deferred function containing recovery invocation // - stack[panicIndex] is the code line causing the panic -// - perrors.Short displays the error message along with the code location raising panic -// - perrors.Long displays all available information about the error +// - [perrors.Short] displays the error message along with the code location raising panic +// - [perrors.Long] displays all available information about the error func IsPanic(err error) (isPanic bool, stack pruntime.StackSlice, recoveryIndex, panicIndex int) { stack0 := errorglue.GetInnerMostStack(err) if len(stack0) == 0 { - return // have no stack, assume not a panic + return // error has no stack attached, cannot detect panic } isPanic, recoveryIndex, panicIndex = errorglue.Indices(stack0) stack = stack0 diff --git a/pio/context-copier.go b/pio/context-copier.go index d7e6b588..05657279 100644 --- a/pio/context-copier.go +++ b/pio/context-copier.go @@ -73,7 +73,7 @@ func (c *ContextCopier) Configuration() ( func (c *ContextCopier) ContextThread() { var err error parl.SendErr(c.errCh, &err) - defer parl.Recover(parl.Annotation(), &err, parl.NoOnError) + defer parl.PanicToErr(&err) select { case <-c.ctx.Done(): diff --git a/pmaps/btree-iterator.go b/pmaps/b-tree-iterator.go similarity index 100% rename from pmaps/btree-iterator.go rename to pmaps/b-tree-iterator.go diff --git a/pmaps/b-tree-map.go b/pmaps/b-tree-map.go new file mode 100644 index 00000000..86669d1b --- /dev/null +++ b/pmaps/b-tree-map.go @@ -0,0 +1,138 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package pmaps + +import ( + "github.com/google/btree" + "golang.org/x/exp/constraints" +) + +const ( + BtreeDegree = 6 // each level has 2^6 children: 64 +) + +type SameFunc[V any] func(a, b V) (isSameRank bool) + +// BTreeMap is a reusable and promotable mapping +// whose values are provided in custom order +// - mapping implementation is Go Map +// - ordering structure is B-tree +// - B-tree offers: +// - — avoiding vector-copy of large sorted slices which is slow and +// - — avoiding linear traversal of linked-lists which is slow and +// - — is a more efficient structure than binary tree +// - Put is implemented by consumers that can compare V values +type BTreeMap[K comparable, V any] struct { + map2[K, V] // Get() Length() Range() + tree *btree.BTreeG[V] +} + +// NewBTreeMap returns a mapping whose values are provided in custom order +// - btree.Ordered does not include ~uintptr +func NewBTreeMap[K comparable, V btree.Ordered]() (orderedMap *BTreeMap[K, V]) { + return newBTreeMap[K, V](btree.NewOrderedG[V](BtreeDegree)) +} + +// NewBTreeMapAny returns a mapping whose values are provided in custom order +// - for uintptr +func NewBTreeMapAny[K comparable, V any](less btree.LessFunc[V]) (orderedMap *BTreeMap[K, V]) { + return newBTreeMap[K, V](btree.NewG[V](BtreeDegree, less)) +} + +// newBTreeMap creates using existing B-tree +func newBTreeMap[K comparable, V any](tree *btree.BTreeG[V]) (orderedMap *BTreeMap[K, V]) { + return &BTreeMap[K, V]{ + map2: *newMap[K, V](), + tree: tree, + } +} + +// constraints.Ordered includes ~uintptr +type _ interface { + // ~int | ~int8 | ~int16 | ~int32 | ~int64 + constraints.Signed + // ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr + constraints.Unsigned + // ~float32 | ~float64 + constraints.Float + // ~string + constraints.Ordered +} + +// btree.Ordered does not include ~uintptr +type BtreeOrdered interface { + // ~int | ~int8 | ~int16 | ~int32 | ~int64 | + // ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | + // ~float32 | ~float64 | ~string + btree.Ordered +} + +// Delete removes mapping using key K. +// - if key K is not mapped, the map is unchanged. +// - O(log n) +func (m *BTreeMap[K, V]) Delete(key K) { + + // no-op: delete non-existent mapping + var existing, hasExisting = m.m2.Get(key) + if !hasExisting { + return // maping does not exist return + } + + // set mapped value to zero value + var zeroValue V + m.m2.Put(key, zeroValue) + + // delete mapping + m.m2.Delete(key) + m.tree.Delete(existing) // delete from sort order +} + +// Clear empties the map +// - clears by re-initializing the map +// - when instead ranging and deleting all keys, +// the unused size of the map is retained +func (m *BTreeMap[K, V]) Clear() { + m.m2.Clear() + m.tree.Clear(false) +} + +// Clone returns a shallow clone of the map +// - clone is done by ranging all keys +func (m *BTreeMap[K, V]) Clone() (clone *BTreeMap[K, V]) { + return &BTreeMap[K, V]{ + map2: *m.map2.clone(), + tree: m.tree.Clone(), + } +} + +// List provides mapped values in order +// - n zero or missing means all items +// - n non-zero means this many items capped by length +func (m *BTreeMap[K, V]) List(n ...int) (list []V) { + + // empty map case + var length = m.m2.Length() + if length == 0 { + return + } + + // non-zero list length [1..length] to use + var nUse int + // provided n capped by length + if len(n) > 0 { + if nUse = n[0]; nUse > length { + nUse = length + } + } + // default to full length + if nUse == 0 { + nUse = length + } + + list = NewBtreeIterator(m.tree).Iterate(nUse) + + return +} diff --git a/pmaps/b-tree-map2.go b/pmaps/b-tree-map2.go new file mode 100644 index 00000000..9b07e062 --- /dev/null +++ b/pmaps/b-tree-map2.go @@ -0,0 +1,44 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package pmaps + +import "github.com/google/btree" + +type btreeMap[K comparable, V any] struct { + BTreeMap[K, V] +} + +// func newBTreeMap2[K comparable, V btree.Ordered]() (m *btreeMap[K, V]) { +// return &btreeMap[K, V]{BTreeMap: *NewBTreeMap[K, V]()} +// } + +func newBTreeMap2Any[K comparable, V any](less btree.LessFunc[V]) (m *btreeMap[K, V]) { + return &btreeMap[K, V]{BTreeMap: *NewBTreeMapAny[K, V](less)} +} + +func (m *btreeMap[K, V]) Clone() (clone *btreeMap[K, V]) { + return &btreeMap[K, V]{BTreeMap: *m.BTreeMap.Clone()} +} + +func (m *btreeMap[K, V]) put(key K, value V, sameFunc SameFunc[V]) { + + // existing mapping + if existing, hasExisting := m.Get(key); hasExisting { + + //no-op: key exist with equal rank + if sameFunc(value, existing) { + return // exists with equal sort order return: nothing to do + } + + // update: key exists but value sorts differently + // - remove from sorted index + m.tree.Delete(existing) + } + + // create mapping or update mapped value + m.m2.Put(key, value) + m.tree.ReplaceOrInsert(value) // create in sort order +} diff --git a/pmaps/cmp-less.go b/pmaps/cmp-less.go deleted file mode 100644 index a0830b76..00000000 --- a/pmaps/cmp-less.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) -ISC License -*/ - -package pmaps - -import ( - "cmp" - - "github.com/google/btree" -) - -// CmpLess provides a Less function based on a comparison function -// - Less returns true when a < b -// - Less returns false for equal or b > a -// - the Less function is defined by github.com/google/btree and cmp packages -// - the compare function is defined by the cmp package -// - CmpLess is required since a constructor cannot store references -// to its own fields in order to avoid memory leaks and corrupt behaviors. -// The constructor can, however, store pointers to a CmpLess object -type CmpLess[V any] struct { - cmp func(a, b V) (result int) -} - -// NewCmpLess returns a Less function object from a comparison function -func NewCmpLess[V any](cmp func(a, b V) (result int)) (cmpLess *CmpLess[V]) { - return &CmpLess[V]{cmp: cmp} -} - -// func(a, b T) bool -var _ btree.LessFunc[int] - -// Less reports whether x is less than y -// - false for equal -var _ = cmp.Less[int] - -// Compare returns -// - -1 if x is less than y, -// - 0 if x equals y, -// - +1 if x is greater than y. -var _ = cmp.Compare[int] - -// Less is a Less function based on a compare function -func (c *CmpLess[V]) Less(a, b V) (aIsFirst bool) { - return c.cmp(a, b) < 0 -} diff --git a/pmaps/go-map-size.go b/pmaps/go-map-size.go index fccba460..74262786 100644 --- a/pmaps/go-map-size.go +++ b/pmaps/go-map-size.go @@ -35,7 +35,7 @@ const ( // Source code: // - the map source code part of the runtime package is available online: // - — https://go.googlesource.com/go/+/refs/heads/master/src/runtime/map.go -// - runtime source is typically installed on a computer that has go: +// - runtime source is typically installed on a computer that has Go: // - — module directory: …libexec/src, package directory: runtime // - — on macOS homebrew similar to: …/homebrew/Cellar/go/1.20.2/libexec/src func GoMapSize[K comparable, V any](m map[K]V) (size uint64) { diff --git a/pmaps/key-by-value-map.go b/pmaps/key-by-value-map.go deleted file mode 100644 index 40805367..00000000 --- a/pmaps/key-by-value-map.go +++ /dev/null @@ -1,66 +0,0 @@ -/* -© 2022–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) -ISC License -*/ - -// KeyByValueMap is a mapping whose keys are provided in value order. -package pmaps - -import ( - "github.com/haraldrudell/parl/parli" - - "github.com/haraldrudell/parl/pslices" - "golang.org/x/exp/constraints" -) - -// KeyByValueMap is a mapping whose keys are provided in value order. -type KeyByValueMap[K comparable, V constraints.Ordered] struct { - Map[K, V] - list parli.Ordered[K] -} - -// NewKeyByValueMap returns a mapping whose keys are provided in value order. -func NewKeyByValueMap[K comparable, V constraints.Ordered]() (m *KeyByValueMap[K, V]) { - k := KeyByValueMap[K, V]{Map: *NewMap[K, V]()} - k.list = pslices.NewOrderedAny(k.cmp) - return &k -} - -// Put saves or replaces a mapping -func (mp *KeyByValueMap[K, V]) Put(key K, value V) { - mp.Map.Put(key, value) - mp.list.Insert(key) -} - -// Delete removes mapping using key K. -// - if key K is not mapped, the map is unchanged. -// - O(log n) -func (mp *KeyByValueMap[K, V]) Delete(key K) { - if _, ok := mp.Map.Get(key); ok { - mp.Map.Delete(key) - mp.list.Delete(key) - } -} - -// Clone returns a shallow clone of the map -func (mp *KeyByValueMap[K, V]) Clone() (clone *KeyByValueMap[K, V]) { - return &KeyByValueMap[K, V]{Map: *mp.Map.Clone(), list: mp.list.Clone()} -} - -// List provides the mapped values in order -// - O(n) -func (mp *KeyByValueMap[K, V]) List(n ...int) (list []K) { - return mp.list.List(n...) -} - -// order keys by their corresponding value -func (mp *KeyByValueMap[K, V]) cmp(a, b K) (result int) { - aV, _ := mp.Map.Get(a) - bV, _ := mp.Map.Get(b) - if aV < bV { - return -1 - } else if aV > bV { - return 1 - } - return 0 -} diff --git a/pmaps/key-ins-ordered-map.go b/pmaps/key-ins-ordered-map.go deleted file mode 100644 index 1a52c22e..00000000 --- a/pmaps/key-ins-ordered-map.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -© 2022–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) -ISC License -*/ - -// KeyInsOrderedMap is a mapping whose keys are provided in insertion order. -package pmaps - -import ( - "golang.org/x/exp/constraints" - "golang.org/x/exp/slices" -) - -// KeyInsOrderedMap is a mapping whose keys are provided in insertion order. -type KeyInsOrderedMap[K constraints.Ordered, V any] struct { - Map[K, V] - list []K -} - -// NewKeyInsOrderedMap is a mapping whose keys are provided in insertion order. -func NewKeyInsOrderedMap[K constraints.Ordered, V any]() (orderedMap *KeyInsOrderedMap[K, V]) { - return &KeyInsOrderedMap[K, V]{Map: *NewMap[K, V]()} -} - -// Put saves or replaces a mapping -func (mp *KeyInsOrderedMap[K, V]) Put(key K, value V) { - if _, ok := mp.Map.Get(key); !ok { - mp.list = append(mp.list, key) - } - mp.Map.Put(key, value) -} - -// Delete removes mapping using key K. -// - if key K is not mapped, the map is unchanged. -// - O(log n) -func (mp *KeyInsOrderedMap[K, V]) Delete(key K) { - mp.Map.Delete(key) - if i := slices.Index(mp.list, key); i != -1 { - mp.list = slices.Delete(mp.list, i, i+1) - } -} - -// Clone returns a shallow clone of the map -func (mp *KeyInsOrderedMap[K, V]) Clone() (clone *KeyInsOrderedMap[K, V]) { - return &KeyInsOrderedMap[K, V]{Map: *mp.Map.Clone(), list: slices.Clone(mp.list)} -} - -// List provides the mapped values in order -// - O(n) -func (mp *KeyInsOrderedMap[K, V]) List(n ...int) (list []K) { - - // default is entire slice - if len(n) == 0 { - list = slices.Clone(mp.list) - return - } - - // ensure 0 ≦ n ≦ length - n0 := n[0] - if n0 <= 0 { - return - } else if length := len(mp.list); n0 >= length { - list = slices.Clone(mp.list) - return - } - - list = slices.Clone(mp.list[:n0]) - - return -} diff --git a/pmaps/key-ordered-map-func.go b/pmaps/key-ordered-map-func.go deleted file mode 100644 index 79a97d47..00000000 --- a/pmaps/key-ordered-map-func.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) -ISC License -*/ - -// KeyOrderedMapFunc is a mapping whose keys are provided in custom order. -package pmaps - -import ( - "github.com/haraldrudell/parl/parli" - "github.com/haraldrudell/parl/perrors" - "github.com/haraldrudell/parl/pslices" - "golang.org/x/exp/constraints" -) - -// KeyOrderedMapFunc is a mapping whose keys are provided in custom order. -type KeyOrderedMapFunc[K comparable, V any] struct { - Map[K, V] - list parli.Ordered[K] -} - -// NewKeyOrderedMapFunc returns a mapping whose keys are provided in custom order. -func NewKeyOrderedMapFunc[K comparable, V any]( - cmp func(a, b K) (result int), -) (orderedMap *KeyOrderedMapFunc[K, V]) { - return &KeyOrderedMapFunc[K, V]{ - Map: *NewMap[K, V](), - list: pslices.NewOrderedAny(cmp), - } -} - -// NewKeyOrderedMapFunc2 returns a mapping whose keys are provided in order. -func NewKeyOrderedMapFunc2[K constraints.Ordered, V any]( - list parli.Ordered[K], -) (orderedMap *KeyOrderedMapFunc[K, V]) { - if list == nil { - panic(perrors.NewPF("list cannot be nil")) - } else if list.Length() > 0 { - list.Clear() - } - return &KeyOrderedMapFunc[K, V]{ - Map: *NewMap[K, V](), - list: list, - } -} - -// Put saves or replaces a mapping -func (m *KeyOrderedMapFunc[K, V]) Put(key K, value V) { - length0 := m.Map.Length() - m.Map.Put(key, value) - if length0 < m.Map.Length() { - m.list.Insert(key) - } -} - -// Delete removes mapping using key K. -// - if key K is not mapped, the map is unchanged. -// - O(log n) -func (m *KeyOrderedMapFunc[K, V]) Delete(key K) { - m.Map.Delete(key) - m.list.Delete(key) -} - -// Clone returns a shallow clone of the map -func (m *KeyOrderedMapFunc[K, V]) Clear() { - m.Map.Clear() - m.list.Clear() -} - -// Clone returns a shallow clone of the map -func (m *KeyOrderedMapFunc[K, V]) Clone() (clone *KeyOrderedMapFunc[K, V]) { - return &KeyOrderedMapFunc[K, V]{Map: *m.Map.Clone(), list: m.list.Clone()} -} - -// List provides keys in order -// - O(n) -func (m *KeyOrderedMapFunc[K, V]) List(n ...int) (list []K) { - return m.list.List(n...) -} diff --git a/pmaps/key-ordered-map.go b/pmaps/key-ordered-map.go index 4a1ce5fe..791144c2 100644 --- a/pmaps/key-ordered-map.go +++ b/pmaps/key-ordered-map.go @@ -3,32 +3,116 @@ ISC License */ -// KeyOrderedMap is a mapping whose keys are provided in order. package pmaps import ( - "github.com/haraldrudell/parl/pslices" + "github.com/google/btree" "golang.org/x/exp/constraints" ) -// KeyOrderedMap is a mapping whose keys are provided in order. -// - key is ordered type +// KeyOrderedMap is a mapping whose keys are provided in order +// - native Go Map functions: Get Put Delete Length Range +// - convenience methods: Clear Clone +// - order method: List +// - — those methods are implemented because they require access +// to the underlying Go map +// - mapping implementation is Go Map +// - ordering structure is B-tree +// - B-tree offers: +// - — avoiding vector-copy of large sorted slices which is slow and +// - — avoiding linear traversal of linked-lists which is slow and +// - — is a more efficient structure than binary tree type KeyOrderedMap[K constraints.Ordered, V any] struct { - KeyOrderedMapFunc[K, V] + map2[K, V] // Get() Length() Range() + tree *btree.BTreeG[K] } // NewKeyOrderedMap returns a mapping whose keys are provided in order. -func NewKeyOrderedMap[K constraints.Ordered, V any]() (orderedMap *KeyOrderedMap[K, V]) { +func NewKeyOrderedMap[K btree.Ordered, V any]() (orderedMap *KeyOrderedMap[K, V]) { return &KeyOrderedMap[K, V]{ - KeyOrderedMapFunc: *NewKeyOrderedMapFunc2[K, V]( - pslices.NewOrdered[K](), - ), + map2: *newMap[K, V](), + tree: btree.NewOrderedG[K](BtreeDegree), } } +// NewKeyOrderedMap returns a mapping whose keys are provided in order. +func NewKeyOrderedMapOrdered[K constraints.Ordered, V any]() (orderedMap *KeyOrderedMap[K, V]) { + return &KeyOrderedMap[K, V]{ + map2: *newMap[K, V](), + tree: btree.NewG[K](BtreeDegree, Less), + } +} + +func (m *KeyOrderedMap[K, V]) Put(key K, value V) { + + // whether the mapping exists + // - value is not comparable, so if mapping exists, the only + // action is to overwrite existing value + var _, hasExisting = m.m2.Get(key) + + // create or update mapping + m.m2.Put(key, value) + + // if mapping exists, key is already in sort order + if hasExisting { + return // key already exists in order return + } + + m.tree.ReplaceOrInsert(key) +} +func (m *KeyOrderedMap[K, V]) Delete(key K) { + + // no-op: delete non-existent mapping + if _, hasExisting := m.m2.Get(key); !hasExisting { + return // maping does not exist return + } + + // set mapped value to zero value + var zeroValue V + m.m2.Put(key, zeroValue) + + // delete mapping + m.m2.Delete(key) + m.tree.Delete(key) // delete from sort order +} +func (m *KeyOrderedMap[K, V]) Clear() { + m.m2.Clear() + m.tree.Clear(false) +} + // Clone returns a shallow clone of the map func (m *KeyOrderedMap[K, V]) Clone() (clone *KeyOrderedMap[K, V]) { return &KeyOrderedMap[K, V]{ - KeyOrderedMapFunc: *m.KeyOrderedMapFunc.Clone(), + map2: *m.map2.clone(), + tree: m.tree.Clone(), } } + +// List provides mapped values in order +// - n zero or missing means all items +// - n non-zero means this many items capped by length +func (m *KeyOrderedMap[K, V]) List(n ...int) (list []K) { + + // empty map case + var length = m.m2.Length() + if length == 0 { + return + } + + // non-zero list length [1..length] to use + var nUse int + // provided n capped by length + if len(n) > 0 { + if nUse = n[0]; nUse > length { + nUse = length + } + } + // default to full length + if nUse == 0 { + nUse = length + } + + list = NewBtreeIterator(m.tree).Iterate(nUse) + + return +} diff --git a/pmaps/less-ordered.go b/pmaps/less-ordered.go new file mode 100644 index 00000000..51fdef9c --- /dev/null +++ b/pmaps/less-ordered.go @@ -0,0 +1,18 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package pmaps + +import ( + "github.com/google/btree" + "golang.org/x/exp/constraints" +) + +// Less is btree.LessFunc for ordered values +func Less[V constraints.Ordered](a, b V) (aBeforeB bool) { + return a < b +} + +var _ btree.LessFunc[int] = Less[int] diff --git a/pmaps/map.go b/pmaps/map.go index 793dca63..c7a96907 100644 --- a/pmaps/map.go +++ b/pmaps/map.go @@ -21,7 +21,7 @@ func NewMap[K comparable, V any]() (mapping *Map[K, V]) { return &Map[K, V]{m: make(map[K]V)} } -// Get returns the value mapped by key or the V zero-value otherwise. +// Get returns the value mapped by key or the V zero-value otherwise // - ok: true if a mapping was found // - O(1) func (m *Map[K, V]) Get(key K) (value V, ok bool) { @@ -29,7 +29,7 @@ func (m *Map[K, V]) Get(key K) (value V, ok bool) { return } -// Put saves or replaces a mapping +// Put create or replaces a mapping func (m *Map[K, V]) Put(key K, value V) { m.m[key] = value } @@ -59,20 +59,28 @@ func (m *Map[K, V]) Range(rangeFunc func(key K, value V) (keepGoing bool)) { } // Clear empties the map -// - re-initialize the map is faster -// - if ranging and deleting keys, the unused size of the map is retained +// - clears by re-initializing the map +// - when instead ranging and deleting all keys, +// the unused size of the map is retained func (m *Map[K, V]) Clear() { m.m = make(map[K]V) } // Clone returns a shallow clone of the map +// - mp is an optional pointer to an already allocated map instance +// to be used +// - clone is done by ranging all keys func (m *Map[K, V]) Clone(mp ...*Map[K, V]) (clone *Map[K, V]) { + + // clone should point to a destination instance if len(mp) > 0 { clone = mp[0] } if clone == nil { clone = &Map[K, V]{} } + clone.m = maps.Clone(m.m) + return } diff --git a/pmaps/map2.go b/pmaps/map2.go new file mode 100644 index 00000000..7aea44af --- /dev/null +++ b/pmaps/map2.go @@ -0,0 +1,34 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package pmaps + +// map2 is a private promotable field only promoting +// explicit public identifiers Get Length Range +type map2[K comparable, V any] struct { + // m2 protects public identifiers from being promoted + m2 Map[K, V] +} + +// map2 is a private promotable field without public identifiers +func newMap[K comparable, V any]() (m *map2[K, V]) { + return &map2[K, V]{m2: *NewMap[K, V]()} +} + +func (m *map2[K, V]) Get(key K) (value V, ok bool) { + return m.m2.Get(key) +} + +func (m *map2[K, V]) Length() (length int) { + return m.m2.Length() +} + +func (m *map2[K, V]) Range(rangeFunc func(key K, value V) (keepGoing bool)) { + m.m2.Range(rangeFunc) +} + +func (m *map2[K, V]) clone() (clone *map2[K, V]) { + return &map2[K, V]{m2: *m.m2.Clone()} +} diff --git a/pmaps/ordered-map-func.go b/pmaps/ordered-map-func.go index b56ba1be..80021cbe 100644 --- a/pmaps/ordered-map-func.go +++ b/pmaps/ordered-map-func.go @@ -3,25 +3,17 @@ ISC License */ -// OrderedMapFunc is a mapping whose values are provided in custom order. package pmaps import ( "github.com/google/btree" ) -const ( - btreeDegree = 6 // each level has 2^6 children: 64 -) - // OrderedMapFunc is a mapping whose values are provided in custom order -// - cmp(a, b) implements sort order and returns: -// - — a negative number if a should be before b -// - — 0 if a == b, to be defined what the ordering action is -// - — a positive number if a should be after b -// - — the reason to use a cmp function rather than a less function used by btree -// is that the comparison function allows for detecting sort-order duplicates, ie. -// identical items +// - less(a, b) implements sort order and returns: +// - — true if a sorts before b +// - — false if a is of equal rank to b, or a is after b +// - — a equals b must not return true // - mapping implementation is Go Map // - ordering structure is B-tree // - B-tree offers: @@ -29,127 +21,45 @@ const ( // - — avoiding linear traversal of linked-lists which is slow and // - — is a more efficient structure than binary tree type OrderedMapFunc[K comparable, V any] struct { - Map[K, V] // Get() Length() Range() - tree *btree.BTreeG[V] - cmp func(a, b V) (result int) + btreeMap[K, V] // Get() Length() Range() Delete() Clear() List() + // type LessFunc[T any] func(a, b T) bool. + // - less(a, b) implements sort order and returns: + // - — true if a sorts before b + // - — false if a is of equal rank to b, or a is after b + // - — a equals b must not return true + less btree.LessFunc[V] } -// type LessFunc[T any] func(a, b T) bool. -// the less function returns: -// - true when a is before b in the sort order -// - false for equal ordering, identical items -// or btree will fail -var _ btree.LessFunc[int] - // NewOrderedMapFunc returns a mapping whose values are provided in custom order. -// - cmp(a, b) implements sort order and returns: -// - — a negative number if a should be before b -// - — 0 if a == b -// - — a positive number if a should be after b +// - less(a, b) implements sort order and returns: +// - — true if a sorts before b +// - — false if a is of equal rank to b, or a is after b +// - — a equals b must not return true +// - btree.Ordered does not include ~uintptr func NewOrderedMapFunc[K comparable, V any]( - cmp func(a, b V) (result int), + less func(a, b V) (aBeforeB bool), ) (orderedMap *OrderedMapFunc[K, V]) { - var less = NewCmpLess(cmp) return &OrderedMapFunc[K, V]{ - Map: *NewMap[K, V](), - tree: btree.NewG[V](btreeDegree, less.Less), - cmp: cmp, + btreeMap: *newBTreeMap2Any[K, V](less), + less: less, } } -// NewOrderedMapFunc2 returns a mapping whose values are provided in custom order. -// func NewOrderedMapFunc2[K comparable, V any]( -// list parli.Ordered[V], -// ) (orderedMap *OrderedMapFunc[K, V]) { -// if list == nil { -// panic(perrors.NewPF("list cannot be nil")) -// } else if list.Length() > 0 { -// list.Clear() -// } -// return &OrderedMapFunc[K, V]{ -// Map: *NewMap[K, V](), -// list: list, -// } -// } - -// Put saves or replaces a mapping +// Put creates or replaces a mapping func (m *OrderedMapFunc[K, V]) Put(key K, value V) { - - // identical case - var existing, hasExisting = m.Map.Get(key) - if hasExisting && m.cmp(value, existing) == 0 { - return // nothing to do - } - - // update: key exists but value sorts differently - // - remove from sorted index - if hasExisting { - m.tree.Delete(existing) - } - - // new or update - m.Map.Put(key, value) - m.tree.ReplaceOrInsert(value) -} - -// Delete removes mapping using key K. -// - if key K is not mapped, the map is unchanged. -// - O(log n) -func (m *OrderedMapFunc[K, V]) Delete(key K) { - - // does not have case - var existing, hasExisting = m.Map.Get(key) - if !hasExisting { - return - } - - // delete case - var zeroValue V - m.Map.Put(key, zeroValue) - m.Map.Delete(key) - m.tree.Delete(existing) -} - -// Clear empties the map -// - re-initialize the map is faster -// - if ranging and deleting keys, the unused size of the map is retained -func (m *OrderedMapFunc[K, V]) Clear() { - m.Map.Clear() - m.tree.Clear(false) + m.btreeMap.put(key, value, m.sameFunc) } // Clone returns a shallow clone of the map +// - clone is done by ranging all keys func (m *OrderedMapFunc[K, V]) Clone() (clone *OrderedMapFunc[K, V]) { return &OrderedMapFunc[K, V]{ - Map: *m.Map.Clone(), - tree: m.tree.Clone(), - cmp: m.cmp, + btreeMap: *m.btreeMap.Clone(), + less: m.less, } } -// List provides the mapped values in order -// - n zero or missing means all items -// - n non-zero means this many items capped by length -func (m *OrderedMapFunc[K, V]) List(n ...int) (list []V) { - - // empty map case - var length = m.Map.Length() - if length == 0 { - return - } - - // non-zero list length [1..length] to use - var nUse int - if len(n) > 0 { - if nUse = n[0]; nUse > length { - nUse = length - } - } - if nUse == 0 { - nUse = length - } - - list = NewBtreeIterator(m.tree).Iterate(nUse) - - return +// sameFunc returns if the two values have equal sort-order rank +func (m *OrderedMapFunc[K, V]) sameFunc(value1, value2 V) (isSameRank bool) { + return !m.less(value1, value2) && !m.less(value2, value1) } diff --git a/pmaps/ordered-map-func2.go b/pmaps/ordered-map-func2.go new file mode 100644 index 00000000..e5e4180c --- /dev/null +++ b/pmaps/ordered-map-func2.go @@ -0,0 +1,24 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package pmaps + +import "github.com/google/btree" + +type orderedMapFunc[K comparable, V any] struct { + OrderedMapFunc[K, V] +} + +func newOrderedMapFunc[K comparable, V btree.Ordered]() (m *orderedMapFunc[K, V]) { + return &orderedMapFunc[K, V]{ + OrderedMapFunc: *NewOrderedMapFunc[K, V](nil), + } +} + +func newOrderedMapFuncUintptr[K comparable, V ~uintptr](less func(a V, b V) (aBeforeB bool)) (m *orderedMapFunc[K, V]) { + return &orderedMapFunc[K, V]{ + OrderedMapFunc: *NewOrderedMapFunc[K, V](less), + } +} diff --git a/pmaps/ordered-map-func_test.go b/pmaps/ordered-map-func_test.go index 6b7291d2..33445287 100644 --- a/pmaps/ordered-map-func_test.go +++ b/pmaps/ordered-map-func_test.go @@ -20,13 +20,8 @@ func (v *V) String() (s string) { return strconv.Itoa(v.value) } -func testCmp(a, b *V) (result int) { - if a.value < b.value { - return -1 - } else if a.value > b.value { - return 1 - } - return 0 +func testLess(a, b *V) (aBeforeB bool) { + return a.value < b.value } func TestNewOrderedMapFunc(t *testing.T) { @@ -39,7 +34,7 @@ func TestNewOrderedMapFunc(t *testing.T) { var m OrderedMapFunc[int, *V] var vPointers []*V - m = *NewOrderedMapFunc[int, *V](testCmp) + m = *NewOrderedMapFunc[int, *V](testLess) // put in order 2, 3, 1 m.Put(v2.value, &v2) t.Logf("%v", m.List()) @@ -54,9 +49,9 @@ func TestNewOrderedMapFunc(t *testing.T) { // vPointers: [1 3] if debug { - var mapContents = make([]string, len(m.Map.m)) + var mapContents = make([]string, len(m.m2.m)) var i = 0 - for key, value := range m.Map.m { + for key, value := range m.m2.m { mapContents[i] = fmt.Sprintf("%d: %d", key, value.value) i++ } diff --git a/pmaps/ordered-map.go b/pmaps/ordered-map.go index 8f36e658..7184483f 100644 --- a/pmaps/ordered-map.go +++ b/pmaps/ordered-map.go @@ -6,20 +6,47 @@ ISC License // OrderedMap is a mapping whose values are provided in order. package pmaps -// TODO 230923 fix when OrderedMapFunc refactored with btree -// // OrderedMap is a mapping whose values are provided in order. -// type OrderedMap[K comparable, V constraints.Ordered] struct { -// OrderedMapFunc[K, V] // reusable map with values provided in order -// } - -// // NewOrderedMap returns a mapping whose values are provided in order. -// func NewOrderedMap[K comparable, V constraints.Ordered]() (orderedMap *OrderedMap[K, V]) { -// return &OrderedMap[K, V]{ -// OrderedMapFunc: *NewOrderedMapFunc2[K, V](pslices.NewOrdered[V]()), -// } -// } - -// // Clone returns a shallow clone of the map -// func (m *OrderedMap[K, V]) Clone() (clone *OrderedMap[K, V]) { -// return &OrderedMap[K, V]{OrderedMapFunc: *m.OrderedMapFunc.Clone()} -// } +import ( + "github.com/google/btree" + "golang.org/x/exp/constraints" +) + +// OrderedMap is a mapping whose values are provided in order +// - mapping implementation is Go Map +// - ordering structure is B-tree +// - constraints.Ordered: integer float string +// - B-tree offers: +// - — avoiding vector-copy of large sorted slices which is slow and +// - — avoiding linear traversal of linked-lists which is slow and +// - — is a more efficient structure than binary tree +type OrderedMap[K comparable, V constraints.Ordered] struct { + orderedMapFunc[K, V] // Get() Length() Range() Delete() Clear() Clone() List() +} + +// NewOrderedMap returns a map for btree.Ordered, ie. not ~uintptr +func NewOrderedMap[K comparable, V btree.Ordered]() (orderedMap *OrderedMap[K, V]) { + return &OrderedMap[K, V]{orderedMapFunc: *newOrderedMapFunc[K, V]()} +} + +// NewOrderedMapUintptr returns a map for ~uintptr +func NewOrderedMapUintptr[K comparable, V ~uintptr]() (orderedMap *OrderedMap[K, V]) { + return &OrderedMap[K, V]{orderedMapFunc: *newOrderedMapFuncUintptr[K, V](orderedLess[V])} +} + +var _ btree.LessFunc[int] = orderedLess[int] + +// orderedLess is btree.LessFunc for ordered types +// - btree.Ordered does not include ~uintptr +func orderedLess[T constraints.Ordered](a, b T) (aBeforeB bool) { + return a < b +} + +// Put creates or replaces a mapping +func (m *OrderedMap[K, V]) Put(key K, value V) { + m.orderedMapFunc.btreeMap.put(key, value, m.sameFunc) +} + +// sameFunc returns if the two values have equal sort-order rank +func (m *OrderedMap[K, V]) sameFunc(value1, value2 V) (isSameRank bool) { + return value1 == value2 +} diff --git a/pmaps/rwmap.go b/pmaps/rwmap.go index fa6e8a05..c9658da3 100644 --- a/pmaps/rwmap.go +++ b/pmaps/rwmap.go @@ -9,20 +9,21 @@ package pmaps import ( "github.com/haraldrudell/parl/parli" - "github.com/haraldrudell/parl/perrors" ) // RWMap is a one-liner thread-safe mapping. // RWMap implements [parli.ThreadSafeMap][K comparable, V any]. -// - GetOrCreate method is an atomic, thread-safe operation as opposed to -// Get-then-Put -// - Swap and PutIf are atomic, thread-safe operations +// - GetOrCreate method is an atomic, thread-safe operation +// as opposed to Get-then-Put +// - PutIf is atomic, thread-safe operation +// - native Go map functions: Get Put Delete Length Range +// - convenience methods: Clone Clone2 Clear +// - order functions: List Keys // - V is copied so if size of V is large or V contains locks, use pointer // - RWMap uses reader/writer mutual exclusion lock for slightly higher performance. -// - RWMap must be in same package as PMap // - Get methods are O(1) type RWMap[K comparable, V any] struct { - ThreadSafeMap[K, V] + threadSafeMap[K, V] // Clear() } // NewRWMap returns a thread-safe map implementation @@ -32,56 +33,7 @@ func NewRWMap[K comparable, V any]() (rwMap parli.ThreadSafeMap[K, V]) { // NewRWMap2 returns a thread-safe map implementation func NewRWMap2[K comparable, V any]() (rwMap *RWMap[K, V]) { - return &RWMap[K, V]{ThreadSafeMap: *NewThreadSafeMap[K, V]()} -} - -// GetOrCreate returns an item from the map if it exists otherwise creates it. -// - newV or makeV are invoked in the critical section, ie. these functions -// may not access the map or deadlock -// - if a key is mapped, its value is returned -// - otherwise, if newV and makeV are both nil, nil is returned. -// - otherwise, if newV is present, it is invoked to return a pointer ot a value. -// A nil return value from newV causes panic. A new mapping is created using -// the value pointed to by the newV return value. -// - otherwise, a mapping is created using whatever makeV returns -// - newV and makeV may not access the map. -// The map’s write lock is held during their execution -// - GetOrCreate is an atomic, thread-safe operation -// - value insert is O(log n) -func (rw *RWMap[K, V]) GetOrCreate( - key K, - newV func() (value *V), - makeV func() (value V), -) (value V, ok bool) { - rw.lock.Lock() - defer rw.lock.Unlock() - - // try existing mapping - if value, ok = rw.m[key]; ok { - return // mapping exists return - } - - // create using newV - if newV != nil { - pt := newV() - if pt == nil { - panic(perrors.NewPF("newV returned nil")) - } - value = *pt - rw.m[key] = value - ok = true - return // created using newV return - } - - // create using makeV - if makeV != nil { - value = makeV() - rw.m[key] = value - ok = true - return // created using makeV return - } - - return // no key, no newV or makeV: nil return + return &RWMap[K, V]{threadSafeMap: *newThreadSafeMap[K, V]()} } // Putif is conditional Put depending on the return value from the putIf function. @@ -90,99 +42,68 @@ func (rw *RWMap[K, V]) GetOrCreate( // - if key exists and putIf returns false, the put is not carried out and wasNewKey is false // - during PutIf, the map cannot be accessed and the map’s write-lock is held // - PutIf is an atomic, thread-safe operation -func (rw *RWMap[K, V]) PutIf(key K, value V, putIf func(value V) (doPut bool)) (wasNewKey bool) { - rw.lock.Lock() - defer rw.lock.Unlock() +func (m *RWMap[K, V]) PutIf(key K, value V, putIf func(value V) (doPut bool)) (wasNewKey bool) { + defer m.m2.Lock()() - existing, keyExists := rw.m[key] + existing, keyExists := m.m2.Get(key) wasNewKey = !keyExists if keyExists && putIf != nil && !putIf(existing) { return // putIf false return: this value should not be updated } - rw.m[key] = value + m.m2.Put(key, value) return } // Clone returns a shallow clone of the map -func (rw *RWMap[K, V]) Clone() (clone parli.ThreadSafeMap[K, V]) { - return rw.Clone2() +func (m *RWMap[K, V]) Clone() (clone parli.ThreadSafeMap[K, V]) { + return m.Clone2() } // Clone returns a shallow clone of the map -func (rw *RWMap[K, V]) Clone2() (clone *RWMap[K, V]) { - return &RWMap[K, V]{ThreadSafeMap: *rw.ThreadSafeMap.Clone()} -} - -// Swap replaces the map with otherMap and returns the current map in previousMap -// - if otherMap is not RWMap, no swap takes place and previousMap is nil -// - Swap is an atomic, thread-safe operation -func (rw *RWMap[K, V]) Swap(otherMap parli.ThreadSafeMap[K, V]) (previousMap parli.ThreadSafeMap[K, V]) { - - // check otherMap - replacingRWMap, ok := otherMap.(*RWMap[K, V]) - if !ok || replacingRWMap.m == nil { - return // otherMap of bad type - } - - // prepare previousMap - replacedRWMap := &RWMap[K, V]{} - previousMap = replacedRWMap - - rw.lock.Lock() - defer rw.lock.Unlock() - - // swap - replacedRWMap.m = rw.m - rw.m = replacingRWMap.m +func (m *RWMap[K, V]) Clone2() (clone *RWMap[K, V]) { + defer m.m2.RLock()() - return // swap complete return + return &RWMap[K, V]{threadSafeMap: *m.threadSafeMap.clone()} } -// List provides the mapped values, undefined ordering +// Keys provides the mapping keys, undefined ordering // - O(n) -func (rw *RWMap[K, V]) List() (list []V) { - rw.lock.RLock() - defer rw.lock.RUnlock() - - list = make([]V, len(rw.m)) - i := 0 - for _, v := range rw.m { - list[i] = v - i++ - } - - return -} - -// List provides keys, undefined ordering -// - O(n) -func (rw *RWMap[K, V]) Keys(n ...int) (list []K) { - +// - invoked while holding RLock or Lock +func (m *RWMap[K, V]) Keys(n ...int) (list []K) { // get n var n0 int if len(n) > 0 { n0 = n[0] } - - rw.lock.RLock() - defer rw.lock.RUnlock() - - // get length - var length int = len(rw.m) - if n0 > 0 && n0 < length { - length = n0 + defer m.m2.RLock()() + + // handle n + var length = m.m2.Length() + if n0 == 0 { + n0 = length + } else if n0 > length { + n0 = length } + list = make([]K, n0) - list = make([]K, length) - i := 0 - for key := range rw.m { - list[i] = key - i++ - if i >= length { - break - } + var r = ranger[K, V]{ + list: list, + n: n0, } + m.m2.Range(r.rangeFunc) + + return +} + +type ranger[K comparable, V any] struct { + list []K + i, n int +} +func (r *ranger[K, V]) rangeFunc(key K, value V) (keepGoing bool) { + r.list[r.i] = key + r.i++ + keepGoing = r.i < r.n return } diff --git a/pmaps/thread-safe-key-ordered-by-value-map.go b/pmaps/thread-safe-key-ordered-by-value-map.go deleted file mode 100644 index 23e5fbd7..00000000 --- a/pmaps/thread-safe-key-ordered-by-value-map.go +++ /dev/null @@ -1,117 +0,0 @@ -/* -© 2022–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) -ISC License -*/ - -// ThreadSafeKeyOrderedByValueMap is a mapping whose keys can be provided in value order. Thread-safe. -package pmaps - -import ( - "sync" - - "golang.org/x/exp/constraints" -) - -// ThreadSafeKeyOrderedByValueMap is a mapping whose keys can be provided in value order. Thread-safe. -type ThreadSafeKeyOrderedByValueMap[K comparable, V constraints.Ordered] struct { - lock sync.RWMutex - KeyByValueMap[K, V] -} - -func NewThreadSafeKeyOrderedByValueMap[K comparable, V constraints.Ordered]() (m *ThreadSafeKeyOrderedByValueMap[K, V]) { - return &ThreadSafeKeyOrderedByValueMap[K, V]{ - KeyByValueMap: *NewKeyByValueMap[K, V](), - } -} - -func (mp *ThreadSafeKeyOrderedByValueMap[K, V]) Get(key K) (value V, ok bool) { - mp.lock.RLock() - defer mp.lock.RUnlock() - - return mp.KeyByValueMap.Get(key) -} - -// Put saves or replaces a mapping -func (mp *ThreadSafeKeyOrderedByValueMap[K, V]) Put(key K, value V) { - mp.lock.Lock() - defer mp.lock.Unlock() - - mp.KeyByValueMap.Put(key, value) -} - -// Put saves or replaces a mapping -func (mp *ThreadSafeKeyOrderedByValueMap[K, V]) PutIf(key K, value V, putIf func(value V) (doPut bool)) (wasNewKey bool) { - mp.lock.Lock() - defer mp.lock.Unlock() - - existing, keyExists := mp.m[key] - wasNewKey = !keyExists - if keyExists && putIf != nil && !putIf(existing) { - return // putIf false return: this value should not be updated - } - mp.m[key] = value - - return -} - -// Delete removes mapping using key K. -// - if key K is not mapped, the map is unchanged. -// - O(log n) -func (mp *ThreadSafeKeyOrderedByValueMap[K, V]) Delete(key K) { - mp.lock.Lock() - defer mp.lock.Unlock() - - mp.KeyByValueMap.Delete(key) -} - -// Delete removes mapping using key K. -// - if key K is not mapped, the map is unchanged. -// - O(log n) -func (mp *ThreadSafeKeyOrderedByValueMap[K, V]) DeleteFirst() (key K) { - mp.lock.Lock() - defer mp.lock.Unlock() - - keyList := mp.List(1) - if len(keyList) == 0 { - return // no oldest key to delete - } - - key = keyList[0] - mp.KeyByValueMap.Delete(key) - - return -} - -// Clear empties the map -func (mp *ThreadSafeKeyOrderedByValueMap[K, V]) Clear() { - mp.lock.Lock() - defer mp.lock.Unlock() - - mp.KeyByValueMap.Clear() -} - -func (mp *ThreadSafeKeyOrderedByValueMap[K, V]) Length() (length int) { - mp.lock.RLock() - defer mp.lock.RUnlock() - - return mp.KeyByValueMap.Length() -} - -// Clone returns a shallow clone of the map -func (mp *ThreadSafeKeyOrderedByValueMap[K, V]) Clone() (clone *ThreadSafeKeyOrderedByValueMap[K, V]) { - mp.lock.RLock() - defer mp.lock.RUnlock() - - return &ThreadSafeKeyOrderedByValueMap[K, V]{ - KeyByValueMap: *mp.KeyByValueMap.Clone(), - } -} - -// List provides the mapped values in order -// - O(n) -func (mp *ThreadSafeKeyOrderedByValueMap[K, V]) List(n ...int) (list []K) { - mp.lock.RLock() - defer mp.lock.RUnlock() - - return mp.KeyByValueMap.List(n...) -} diff --git a/pmaps/thread-safe-key-ordered-map.go b/pmaps/thread-safe-key-ordered-map.go deleted file mode 100644 index f9ed16ac..00000000 --- a/pmaps/thread-safe-key-ordered-map.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -© 2022–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) -ISC License -*/ - -package pmaps - -import ( - "github.com/haraldrudell/parl/parli" - "github.com/haraldrudell/parl/pslices" - "golang.org/x/exp/constraints" - "golang.org/x/exp/maps" -) - -type ThreadSafeKeyOrderedMap[K constraints.Ordered, V any] struct { - ThreadSafeMap[K, V] - list parli.Ordered[K] -} - -// NewThreadSafeKeyOrderedMap returns a mapping whose keys are provided in custom order. -func NewThreadSafeKeyOrderedMap[K constraints.Ordered, V any]() (orderedMap *ThreadSafeKeyOrderedMap[K, V]) { - return &ThreadSafeKeyOrderedMap[K, V]{ - ThreadSafeMap: *NewThreadSafeMap[K, V](), - list: pslices.NewOrdered[K](), - } -} - -// Put saves or replaces a mapping -func (m *ThreadSafeKeyOrderedMap[K, V]) Put(key K, value V) { - m.ThreadSafeMap.lock.Lock() - defer m.ThreadSafeMap.lock.Unlock() - - mp := m.ThreadSafeMap.m - length0 := len(mp) - mp[key] = value - if length0 < len(mp) { - m.list.Insert(key) - } -} - -// Delete removes mapping using key K. -// - if key K is not mapped, the map is unchanged. -// - O(log n) -func (m *ThreadSafeKeyOrderedMap[K, V]) Delete(key K) { - m.ThreadSafeMap.lock.Lock() - defer m.ThreadSafeMap.lock.Unlock() - - delete(m.ThreadSafeMap.m, key) - m.list.Delete(key) -} - -// Clone returns a shallow clone of the map -func (m *ThreadSafeKeyOrderedMap[K, V]) Clear() { - m.ThreadSafeMap.lock.Lock() - defer m.ThreadSafeMap.lock.Unlock() - - m.ThreadSafeMap.m = make(map[K]V) - m.list.Clear() -} - -// Clone returns a shallow clone of the map -func (m *ThreadSafeKeyOrderedMap[K, V]) Clone() (clone *ThreadSafeKeyOrderedMap[K, V]) { - clone = NewThreadSafeKeyOrderedMap[K, V]() - m.ThreadSafeMap.lock.RLock() - defer m.ThreadSafeMap.lock.RUnlock() - - clone.ThreadSafeMap.m = maps.Clone(m.ThreadSafeMap.m) - clone.list = m.list.Clone() - return -} - -// List provides keys in order -// - O(n) -func (m *ThreadSafeKeyOrderedMap[K, V]) List(n ...int) (list []K) { - m.ThreadSafeMap.lock.RLock() - defer m.ThreadSafeMap.lock.RUnlock() - - return m.list.List(n...) -} diff --git a/pmaps/thread-safe-map.go b/pmaps/thread-safe-map.go index 3ebca2ed..8a8fd8a8 100644 --- a/pmaps/thread-safe-map.go +++ b/pmaps/thread-safe-map.go @@ -3,8 +3,6 @@ ISC License */ -// RWMap is a one-liner thread-safe mapping. -// RWMap implements [parli.ThreadSafeMap][K comparable, V any]. package pmaps import ( @@ -13,51 +11,73 @@ import ( "golang.org/x/exp/maps" ) -// ThreadSafeMap is a thread-safe mapping that is reusable -// - ThreadSafeMap uses reader/writer mutual exclusion lock to attain thread-safety -// - native functions: Get Put Delete Length Range -// - convenience functions: Clear Clone (need access to the Go map) +const ( + // with [ThreadSafeMap.Delete] sets the mapping value to the + // zero-value prior to delete + SetZeroValue = true + // with [ThreadSafeMap.Clear], the map is cleared using range + // and delete of all keys rather than re-created + RangeDelete = true +) + +// ThreadSafeMap is a thread-safe reusable promotable Go map +// - native Go map functions: Get Put Delete Length Range +// - convenience functions: Clear Clone +// - — those methods need access to the Go map +// - lock control: Lock RLock +// - ThreadSafeMap uses reader/writer mutual exclusion lock for thread-safety type ThreadSafeMap[K comparable, V any] struct { lock sync.RWMutex m map[K]V } // NewThreadSafeMap returns a thread-safe Go map -func NewThreadSafeMap[K comparable, V any]() (pMap *ThreadSafeMap[K, V]) { +func NewThreadSafeMap[K comparable, V any]() (m *ThreadSafeMap[K, V]) { return &ThreadSafeMap[K, V]{m: make(map[K]V)} } +// allows consumers to obtain the write lock +// - returns a function releasing the lock +func (m *ThreadSafeMap[K, V]) Lock() (unlock func()) { + m.lock.Lock() + return m.unlock +} + +// allows consumers to obtain the read lock +// - returns a function releasing the lock +func (m *ThreadSafeMap[K, V]) RLock() (runlock func()) { + m.lock.RLock() + return m.runlock +} + // Get returns the value mapped by key or the V zero-value otherwise. // - the ok return value is true if a mapping was found. +// - invoked while holding Lock or RLock // - O(1) func (m *ThreadSafeMap[K, V]) Get(key K) (value V, ok bool) { - m.lock.RLock() - defer m.lock.RUnlock() - value, ok = m.m[key] - return } -// Put saves or replaces a mapping +// Put creates or replaces a mapping +// - invoked while holding Lock func (m *ThreadSafeMap[K, V]) Put(key K, value V) { - m.lock.Lock() - defer m.lock.Unlock() - m.m[key] = value } -// Delete removes mapping using key K. -// - if key K is not mapped, the map is unchanged. +// Delete removes mapping for key +// - if key is not mapped, the map is unchanged +// - if useZeroValue is [pmaps.SetZeroValue], the mapping value is first +// set to the zero-value. This prevents temporary memory leaks +// when V contains pointers to large objects // - O(log n) +// - invoked while holding Lock func (m *ThreadSafeMap[K, V]) Delete(key K, useZeroValue ...bool) { - m.lock.Lock() - defer m.lock.Unlock() // if doZero is not present and true, regular map delete if len(useZeroValue) == 0 || !useZeroValue[0] { delete(m.m, key) - return + return // non-zero-value delete } // if key mapping does not exist: noop @@ -74,20 +94,16 @@ func (m *ThreadSafeMap[K, V]) Delete(key K, useZeroValue ...bool) { } // Length returns the number of mappings +// - invoked while holding RLock or Lock func (m *ThreadSafeMap[K, V]) Length() (length int) { - m.lock.RLock() - defer m.lock.RUnlock() - return len(m.m) } // Range traverses map bindings // - iterates over map until rangeFunc returns false // - similar to: func (*sync.Map).Range(f func(key any, value any) bool) +// - invoked while holding RLock or Lock func (m *ThreadSafeMap[K, V]) Range(rangeFunc func(key K, value V) (keepGoing bool)) { - m.lock.RLock() - defer m.lock.RUnlock() - for k, v := range m.m { if !rangeFunc(k, v) { return @@ -96,35 +112,65 @@ func (m *ThreadSafeMap[K, V]) Range(rangeFunc func(key K, value V) (keepGoing bo } // Clear empties the map +// - if useRange is RangeDelete, the map is cleared by +// iterating and deleteing all keys +// - invoked while holding Lock func (m *ThreadSafeMap[K, V]) Clear(useRange ...bool) { - m.lock.Lock() - defer m.lock.Unlock() // if useRange is not present and true, clear by re-initialize if len(useRange) == 0 || !useRange[0] { m.m = make(map[K]V) - return + return // re-create clear return } // zero-out and delete each item var zeroValue V - for k, _ := range m.m { + for k := range m.m { m.m[k] = zeroValue delete(m.m, k) } } // Clone returns a shallow clone of the map +// - clone is done by ranging all keys +// - invoked while holding RLock or Lock func (m *ThreadSafeMap[K, V]) Clone() (clone *ThreadSafeMap[K, V]) { - var c ThreadSafeMap[K, V] - clone = &c - c.lock.Lock() // write will holding c lock - defer c.lock.Unlock() + return &ThreadSafeMap[K, V]{m: maps.Clone(m.m)} +} - m.lock.RLock() // prevent changes while range operation in progress - defer m.lock.RUnlock() +// List provides the mapped values, undefined ordering +// - O(n) +// - invoked while holding RLock or Lock +func (m *ThreadSafeMap[K, V]) List(n int) (list []V) { + + // handle n + var length = len(m.m) + if n == 0 { + n = length + } else if n > length { + n = length + } - c.m = maps.Clone(m.m) + // create and populate list + list = make([]V, n) + i := 0 + for _, v := range m.m { + list[i] = v + i++ + if i > n { + break + } + } return } + +// invokes lock.Unlock() +func (m *ThreadSafeMap[K, V]) unlock() { + m.lock.Unlock() +} + +// invokes lock.RUnlock() +func (m *ThreadSafeMap[K, V]) runlock() { + m.lock.RUnlock() +} diff --git a/pmaps/thread-safe-map2.go b/pmaps/thread-safe-map2.go new file mode 100644 index 00000000..3186135e --- /dev/null +++ b/pmaps/thread-safe-map2.go @@ -0,0 +1,124 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package pmaps + +import "github.com/haraldrudell/parl/perrors" + +// threadSafeMap is a private promotable field +// that does not promote any public identifiers +// - native Go map functions: Get Put Delete Length Range +// - convenience methods: clone Clear +// - order methods: List +type threadSafeMap[K comparable, V any] struct { + m2 ThreadSafeMap[K, V] +} + +// newThreadSafeMap returns a thread-safe Go map +func newThreadSafeMap[K comparable, V any]() (m *threadSafeMap[K, V]) { + return &threadSafeMap[K, V]{m2: *NewThreadSafeMap[K, V]()} +} + +// GetOrCreate returns an item from the map if it exists otherwise creates it. +// - newV or makeV are invoked in the critical section, ie. these functions +// may not access the map or deadlock +// - if a key is mapped, its value is returned +// - otherwise, if newV and makeV are both nil, nil is returned. +// - otherwise, if newV is present, it is invoked to return a pointer ot a value. +// A nil return value from newV causes panic. A new mapping is created using +// the value pointed to by the newV return value. +// - otherwise, a mapping is created using whatever makeV returns +// - newV and makeV may not access the map. +// The map’s write lock is held during their execution +// - GetOrCreate is an atomic, thread-safe operation +// - value insert is O(log n) +func (m *threadSafeMap[K, V]) GetOrCreate( + key K, + newV func() (value *V), + makeV func() (value V), +) (value V, ok bool) { + defer m.m2.Lock()() + + // try existing mapping + if value, ok = m.m2.Get(key); ok { + return // mapping exists return + } + + // create using newV + if newV != nil { + pt := newV() + if pt == nil { + panic(perrors.NewPF("newV returned nil")) + } + value = *pt + m.m2.Put(key, value) + ok = true + return // created using newV return + } + + // create using makeV + if makeV != nil { + value = makeV() + m.m2.Put(key, value) + ok = true + return // created using makeV return + } + + return // no key, no newV or makeV: nil return +} + +func (m *threadSafeMap[K, V]) clone() (clone *threadSafeMap[K, V]) { + return &threadSafeMap[K, V]{m2: *m.m2.Clone()} +} +func (m *threadSafeMap[K, V]) Get(key K) (value V, ok bool) { + defer m.m2.RLock()() + + return m.m2.Get(key) +} + +func (m *threadSafeMap[K, V]) Put(key K, value V) { + defer m.m2.Lock()() + + m.m2.Put(key, value) +} + +func (m *threadSafeMap[K, V]) Delete(key K, useZeroValue ...bool) { + defer m.m2.Lock()() + + m.m2.Delete(key, useZeroValue...) +} + +func (m *threadSafeMap[K, V]) Length() (length int) { + defer m.m2.RLock()() + + return m.m2.Length() +} + +func (m *threadSafeMap[K, V]) Range(rangeFunc func(key K, value V) (keepGoing bool)) { + defer m.m2.RLock()() + + m.m2.Range(rangeFunc) +} + +// Clear empties the map +func (m *threadSafeMap[K, V]) Clear(useRange ...bool) { + defer m.m2.Lock()() + + m.m2.Clear(useRange...) +} + +// List provides the mapped values, undefined ordering +// - O(n) +func (m *threadSafeMap[K, V]) List(n ...int) (list []V) { + + // get n + var n0 int + if len(n) > 0 { + n0 = n[0] + } + defer m.m2.RLock()() + + return m.m2.List(n0) +} diff --git a/pmaps/thread-safe-ordered-map-func.go b/pmaps/thread-safe-ordered-map-func.go index 026cc5e6..822c17ef 100644 --- a/pmaps/thread-safe-ordered-map-func.go +++ b/pmaps/thread-safe-ordered-map-func.go @@ -7,182 +7,110 @@ ISC License package pmaps import ( - "github.com/haraldrudell/parl/parli" - "github.com/haraldrudell/parl/perrors" - "github.com/haraldrudell/parl/pslices" - "golang.org/x/exp/constraints" - "golang.org/x/exp/maps" + "github.com/google/btree" ) -// ThreadSafeOrderedMapFunc is a mapping whose values are provided in custom order. Thread-safe. +// ThreadSafeOrderedMapFunc is a mapping whose +// values are provided in custom order. Thread-safe. +// - mapping implementation is Go Map +// - native Go map functions: Get Put Delete Length Range +// - convenience methods: Clone Clear +// - order methods: List +// - ordering structure is B-tree +// - B-tree offers: +// - — avoiding vector-copy of large sorted slices which is slow and +// - — avoiding linear traversal of linked-lists which is slow and +// - — is a more efficient structure than binary tree type ThreadSafeOrderedMapFunc[K comparable, V any] struct { - ThreadSafeMap[K, V] - list parli.Ordered[V] - cmp func(a, b V) (result int) + threadSafeMap[K, V] // Get() Length() Range() + tree *btree.BTreeG[V] + less btree.LessFunc[V] } func NewThreadSafeOrderedMapFunc[K comparable, V any]( - cmp func(a, b V) (result int), + less func(a, b V) (aBeforeB bool), ) (orderedMap *ThreadSafeOrderedMapFunc[K, V]) { return &ThreadSafeOrderedMapFunc[K, V]{ - ThreadSafeMap: *NewThreadSafeMap[K, V](), - list: pslices.NewOrderedAny(cmp), - cmp: cmp, + threadSafeMap: *newThreadSafeMap[K, V](), + tree: btree.NewG(BtreeDegree, less), + less: less, } } -// NewThreadSafeOrderedMapFunc2 returns a mapping whose values are provided in custom order. -func NewThreadSafeOrderedMapFunc2[K comparable, V constraints.Ordered]( - list parli.Ordered[V], -) (orderedMap *ThreadSafeOrderedMapFunc[K, V]) { - if list == nil { - panic(perrors.NewPF("list cannot be nil")) - } else if list.Length() > 0 { - list.Clear() - } - return &ThreadSafeOrderedMapFunc[K, V]{ - ThreadSafeMap: *NewThreadSafeMap[K, V](), - list: list, - cmp: compare[V], - } -} - -func compare[V constraints.Ordered](a, b V) (result int) { - if a < b { - return -1 - } else if a > b { - return 1 - } - return 0 -} +func (m *ThreadSafeOrderedMapFunc[K, V]) Put(key K, value V) { + defer m.m2.Lock()() -// GetOrCreate returns an item from the map if it exists otherwise creates it. -// - newV or makeV are invoked in the critical section, ie. these functions -// may not access the map or deadlock -// - if a key is mapped, its value is returned -// - otherwise, if newV and makeV are both nil, nil is returned. -// - otherwise, if newV is present, it is invoked to return a pointer ot a value. -// A nil return value from newV causes panic. A new mapping is created using -// the value pointed to by the newV return value. -// - otherwise, a mapping is created using whatever makeV returns -// - newV and makeV may not access the map. -// The map’s write lock is held during their execution -// - GetOrCreate is an atomic, thread-safe operation -// - value insert is O(log n) -func (m *ThreadSafeOrderedMapFunc[K, V]) GetOrCreate( - key K, - newV func() (value *V), - makeV func() (value V), -) (value V, ok bool) { - m.lock.Lock() - defer m.lock.Unlock() - - mp := m.ThreadSafeMap.m - - // try existing mapping - if value, ok = mp[key]; ok { - return // mapping exists return - } + // whether the mapping exists + // - value is not comparable, so if mapping exists, the only + // action is to overwrite existing value + var _, hasExisting = m.m2.Get(key) - // create using newV - if newV != nil { - pt := newV() - if pt == nil { - panic(perrors.NewPF("newV returned nil")) - } - value = *pt - mp[key] = value - ok = true - return // created using newV return - } + // create or update mapping + m.m2.Put(key, value) - // create using makeV - if makeV != nil { - value = makeV() - mp[key] = value - ok = true - return // created using makeV return + // if mapping exists, key is already in sort order + if hasExisting { + return // key already exists in order return } - return // no key, no newV or makeV: nil return + m.tree.ReplaceOrInsert(value) } -// Put saves or replaces a mapping -func (m *ThreadSafeOrderedMapFunc[K, V]) Put(key K, value V) { - m.lock.Lock() - defer m.lock.Unlock() +func (m *ThreadSafeOrderedMapFunc[K, V]) Delete(key K, useZeroValue ...bool) { + defer m.m2.Lock()() - length0 := len(m.ThreadSafeMap.m) - m.ThreadSafeMap.m[key] = value - if length0 == len(m.ThreadSafeMap.m) { - return + // no-op: delete non-existent mapping + var existing, hasExisting = m.m2.Get(key) + if !hasExisting { + return // maping does not exist return } - m.list.Insert(value) + + // delete mapping + m.m2.Delete(key, useZeroValue...) + m.tree.Delete(existing) // delete from sort order } +func (m *ThreadSafeOrderedMapFunc[K, V]) Clone() (clone *ThreadSafeOrderedMapFunc[K, V]) { + defer m.m2.RLock()() -// Put saves or replaces a mapping -// - if mapping exists and poutif i non-nil, puIf function is invoked -// - put is only carried out if mapping is new or putIf is non-nil and returns true -func (m *ThreadSafeOrderedMapFunc[K, V]) PutIf(key K, value V, putIf func(value V) (doPut bool)) (wasNewKey bool) { - m.lock.Lock() - defer m.lock.Unlock() - - value0, ok := m.ThreadSafeMap.m[key] - if wasNewKey = !ok; !wasNewKey { - if putIf == nil || !putIf(value0) { - return // existing key, putIf nil or returning false: do nothing - } - if m.cmp(value0, value) != 0 { - m.list.Delete(value0) - m.list.Insert(value) - } + return &ThreadSafeOrderedMapFunc[K, V]{ + threadSafeMap: *m.threadSafeMap.clone(), + tree: m.tree.Clone(), + less: m.less, } - m.ThreadSafeMap.m[key] = value +} +func (m *ThreadSafeOrderedMapFunc[K, V]) Clear(useRange ...bool) { + defer m.m2.Lock()() - return + m.m2.Clear(useRange...) + m.tree.Clear(false) } -// Delete removes mapping using key K. -// - if key K is not mapped, the map is unchanged. -// - O(log n) -func (m *ThreadSafeOrderedMapFunc[K, V]) Delete(key K) { - m.lock.Lock() - defer m.lock.Unlock() +// List provides mapped values in order +// - n zero or missing means all items +// - n non-zero means this many items capped by length +func (m *ThreadSafeOrderedMapFunc[K, V]) List(n ...int) (list []V) { + defer m.m2.RLock()() - var v V - var ok bool - if v, ok = m.ThreadSafeMap.m[key]; !ok { + // empty map case + var length = m.m2.Length() + if length == 0 { return } - delete(m.ThreadSafeMap.m, key) - m.list.Delete(v) -} -// Clear empties the map -func (m *ThreadSafeOrderedMapFunc[K, V]) Clear() { - m.lock.Lock() - defer m.lock.Unlock() - - m.ThreadSafeMap.m = make(map[K]V) - m.list.Clear() -} + // non-zero list length [1..length] to use + var nUse int + // provided n capped by length + if len(n) > 0 { + if nUse = n[0]; nUse > length { + nUse = length + } + } + // default to full length + if nUse == 0 { + nUse = length + } -// Clone returns a shallow clone of the map -func (m *ThreadSafeOrderedMapFunc[K, V]) Clone() (clone *ThreadSafeOrderedMapFunc[K, V]) { - clone = NewThreadSafeOrderedMapFunc[K, V](m.cmp) - m.lock.RLock() - defer m.lock.RUnlock() + list = NewBtreeIterator(m.tree).Iterate(nUse) - clone.ThreadSafeMap.m = maps.Clone(m.ThreadSafeMap.m) - clone.list = m.list.Clone() return } - -// List provides the mapped values in order -// - O(n) -func (m *ThreadSafeOrderedMapFunc[K, V]) List(n ...int) (list []V) { - m.lock.RLock() - defer m.lock.RUnlock() - - return m.list.List(n...) -} diff --git a/pmaps/thread-safe-ordered-map.go b/pmaps/thread-safe-ordered-map.go deleted file mode 100644 index 46097e2e..00000000 --- a/pmaps/thread-safe-ordered-map.go +++ /dev/null @@ -1,33 +0,0 @@ -/* -© 2022–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) -ISC License -*/ - -package pmaps - -import ( - "github.com/haraldrudell/parl/pslices" - "golang.org/x/exp/constraints" -) - -// ThreadSafeOrderedMap is a mapping whose values are provided in order. Thread-safe. -type ThreadSafeOrderedMap[K comparable, V constraints.Ordered] struct { - ThreadSafeOrderedMapFunc[K, V] -} - -func NewThreadSafeOrderedMap[K comparable, V constraints.Ordered]() (orderedMap *ThreadSafeOrderedMap[K, V]) { - return &ThreadSafeOrderedMap[K, V]{ - ThreadSafeOrderedMapFunc: *NewThreadSafeOrderedMapFunc2[K, V]( - pslices.NewOrdered[V](), - )} -} - -// Clone returns a shallow clone of the map -func (m *ThreadSafeOrderedMap[K, V]) Clone() (clone *ThreadSafeOrderedMap[K, V]) { - m.lock.RLock() - defer m.lock.RUnlock() - - return &ThreadSafeOrderedMap[K, V]{ - ThreadSafeOrderedMapFunc: *m.ThreadSafeOrderedMapFunc.Clone(), - } -} diff --git a/pmaps/z-cmp-less.go b/pmaps/z-cmp-less.go new file mode 100644 index 00000000..ae2bbdda --- /dev/null +++ b/pmaps/z-cmp-less.go @@ -0,0 +1,41 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package pmaps + +// CmpLess provides a Less function based on a comparison function +// - Less returns true when a < b +// - Less returns false for equal or b > a +// - the Less function is defined by github.com/google/btree and cmp packages +// - the compare function is defined by the cmp package +// - CmpLess is required since a constructor cannot store references +// to its own fields in order to avoid memory leaks and corrupt behaviors. +// The constructor can, however, store pointers to a CmpLess object +// type CmpLess[V any] struct { +// cmp func(a, b V) (result int) +// } + +// // NewCmpLess returns a Less function object from a comparison function +// func NewCmpLess[V any](cmp func(a, b V) (result int)) (cmpLess *CmpLess[V]) { +// return &CmpLess[V]{cmp: cmp} +// } + +// // func(a, b T) bool +// var _ btree.LessFunc[int] + +// // Less reports whether x is less than y +// // - false for equal +// var _ = cmp.Less[int] + +// // Compare returns +// // - -1 if x is less than y, +// // - 0 if x equals y, +// // - +1 if x is greater than y. +// var _ = cmp.Compare[int] + +// // Less is a Less function based on a compare function +// func (c *CmpLess[V]) Less(a, b V) (aIsFirst bool) { +// return c.cmp(a, b) < 0 +// } diff --git a/pmaps/z-key-by-value-map.go b/pmaps/z-key-by-value-map.go new file mode 100644 index 00000000..787d04d7 --- /dev/null +++ b/pmaps/z-key-by-value-map.go @@ -0,0 +1,127 @@ +/* +© 2022–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +// KeyByValueMap is a mapping whose keys are provided in value order. +package pmaps + +// KeyByValueMap is a mapping whose keys are provided in value order +// - native Go Map functions: Get Put Delete Length Range +// - convenience methods: Clear Clone +// - order method: List +// - — those methods are implemented because they require access +// to the underlying Go map +// - mapping implementation is Go Map +// - ordering structure is B-tree +// - B-tree offers: +// - — avoiding vector-copy of large sorted slices which is slow and +// - — avoiding linear traversal of linked-lists which is slow and +// - — is a more efficient structure than binary tree +// type KeyByValueMap[K comparable, V constraints.Ordered] struct { +// // because tree needs less function, must be pointer +// *keyByValueMap[K, V] // Get() Length() Range() +// tree *btree.BTreeG[K] +// } + +// // NewKeyByValueMap returns a mapping whose keys are provided in value order +// func NewKeyByValueMap[K comparable, V constraints.Ordered]() (mpp *KeyByValueMap[K, V]) { +// // the tree holds K that is not btree.Ordered +// // - therefore a less function is always required +// mp := &keyByValueMap[K, V]{Map: *NewMap[K, V]()} +// return &KeyByValueMap[K, V]{ +// keyByValueMap: mp, +// tree: btree.NewG[K](BtreeDegree, mp.less), +// } +// } + +// // Put saves or replaces a mapping +// func (m *KeyByValueMap[K, V]) Put(key K, value V) { + +// // existing mapping +// if existing, hasExisting := m.Get(key); hasExisting { + +// //no-op: key exist with equal rank +// if value == existing { +// return // exists with equal sort order return: nothing to do +// } + +// // update: key exists but value sorts differently +// // - remove from sorted index +// m.tree.Delete(key) +// } + +// // create mapping or update mapped value +// m.Map.Put(key, value) +// m.tree.ReplaceOrInsert(key) // create in sort order +// } + +// // Delete removes mapping using key K. +// // - if key K is not mapped, the map is unchanged. +// // - O(log n) +// func (m *KeyByValueMap[K, V]) Delete(key K) { +// if _, ok := m.Map.Get(key); !ok { +// return +// } +// m.Map.Delete(key) +// m.tree.Delete(key) +// } + +// // Clone returns a shallow clone of the map +// func (m *KeyByValueMap[K, V]) Clone() (clone *KeyByValueMap[K, V]) { +// return &KeyByValueMap[K, V]{ +// keyByValueMap: &keyByValueMap[K, V]{ +// Map: *m.Map.Clone(), +// }, +// tree: m.tree.Clone(), +// } +// } + +// // Clear empties the map +// // - clears by re-initializing the map +// // - when instead ranging and deleting all keys, +// // the unused size of the map is retained +// func (m *KeyByValueMap[K, V]) Clear() { +// m.Map.Clear() +// m.tree.Clear(false) +// } + +// // List provides the mapped values in order +// // - O(n) +// func (m *KeyByValueMap[K, V]) List(n ...int) (list []K) { + +// // empty map case +// var length = m.Map.Length() +// if length == 0 { +// return +// } + +// // non-zero list length [1..length] to use +// var nUse int +// // provided n capped by length +// if len(n) > 0 { +// if nUse = n[0]; nUse > length { +// nUse = length +// } +// } +// // default to full length +// if nUse == 0 { +// nUse = length +// } + +// list = NewBtreeIterator(m.tree).Iterate(nUse) + +// return +// } + +// // pointed-to struct providing less method +// type keyByValueMap[K comparable, V constraints.Ordered] struct { +// Map[K, V] // Get() Length() Range() +// } + +// // order keys by their corresponding value +// func (m *keyByValueMap[K, V]) less(a, b K) (aBeforeB bool) { +// var aV, _ = m.Map.Get(a) +// var bV, _ = m.Map.Get(b) +// return aV < bV +// } diff --git a/pmaps/z-key-ins-ordered-map.go b/pmaps/z-key-ins-ordered-map.go new file mode 100644 index 00000000..1fc1fd54 --- /dev/null +++ b/pmaps/z-key-ins-ordered-map.go @@ -0,0 +1,108 @@ +/* +© 2022–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +// KeyInsOrderedMap is a mapping whose keys are provided in insertion order. +package pmaps + +// KeyInsOrderedMap is a mapping whose keys are provided in +// insertion order. +// - native Go Map functions: Get Put Delete Length Range +// - convenience methods: Clear Clone +// // - order method: List +// type KeyInsOrderedMap[K comparable, V any] struct { +// // map2 provides O(1) access from key to values +// // - Get() Length() Range() +// map2[K, *keyInsertionIndex[K, V]] +// // B-tree provides iteration over keys in insertion order +// // - ability to delete any item +// // - ability to append items +// // - ability to iterate over item in insertion order +// // - clone, clear +// // - a list offers: +// // - — O(log n) binary search and copy meaning expensive delete +// // - — append enlarges tree by complete reallocation +// // - A B-tree of combined insertion-index and key +// tree *btree.BTreeG[*keyInsertionIndex[K, V]] +// nextIndex atomic.Uint64 +// } + +// type keyInsertionIndex[K, V any] struct { +// index uint64 +// key K +// value V +// } + +// func less[K, V any](a, b *keyInsertionIndex[K, V]) (aBeforeB bool) { +// return a.index < b.index +// } + +// var _ btree.LessFunc[*keyInsertionIndex[int, int]] = less[int, int] + +// // NewKeyInsOrderedMap is a mapping whose keys are provided in insertion order. +// func NewKeyInsOrderedMap[K constraints.Ordered, V any]() (orderedMap *KeyInsOrderedMap[K, V]) { +// return &KeyInsOrderedMap[K, V]{ +// map2: *newMap[K, *keyInsertionIndex[K, V]](), +// tree: btree.NewG[T*keyInsertionIndex[K, V]](BtreeDegree, less[*keyInsertionIndex[K, V]]), +// } +// } + +// // Put saves or replaces a mapping +// func (m *KeyInsOrderedMap[K, V]) Put(key K, value V) { +// if _, ok := m.m2.Get(key); !ok { +// m.tree = append(m.tree, key) +// } +// m.m2.Put(key, value) +// } + +// // Delete removes mapping using key K. +// // - if key K is not mapped, the map is unchanged. +// // - O(log n) +// func (m *KeyInsOrderedMap[K, V]) Delete(key K) { +// m.m2.Delete(key) +// if i := slices.Index(m.tree, key); i != -1 { +// m.tree = slices.Delete(m.tree, i, i+1) +// } +// } + +// // Clone returns a shallow clone of the map +// func (m *KeyInsOrderedMap[K, V]) Clone() (clone *KeyInsOrderedMap[K, V]) { +// return &KeyInsOrderedMap[K, V]{ +// map2: *m.map2.clone(), +// tree: m.tree.Clone(), +// } +// } + +// func (m *KeyInsOrderedMap[K, V]) Clear() { +// m.m2.Clear() +// m.tree.Clear(false) +// } + +// // List provides the mapped values in order +// // - O(n) +// func (m *KeyInsOrderedMap[K, V]) List(n ...int) (list []K) { + +// // empty map case +// var length = m.m2.Length() +// if length == 0 { +// return +// } + +// // non-zero list length [1..length] to use +// var nUse int +// // provided n capped by length +// if len(n) > 0 { +// if nUse = n[0]; nUse > length { +// nUse = length +// } +// } +// // default to full length +// if nUse == 0 { +// nUse = length +// } + +// list = NewBtreeIterator(m.tree).Iterate(nUse) + +// return +// } diff --git a/pnet/http.go b/pnet/http.go index 2b160a45..c27620d3 100644 --- a/pnet/http.go +++ b/pnet/http.go @@ -86,7 +86,7 @@ func (hp *Http) SubListen() (errCh <-chan error) { func (hp *Http) listenerThread() { defer hp.CloseErr() - defer parl.Recover(parl.Annotation(), nil, hp.SendErr) + defer parl.Recover("", nil, hp.SendErr) var didReadyWg bool defer func() { if !didReadyWg { diff --git a/pnet/https.go b/pnet/https.go index b6daade5..62eb8bcf 100644 --- a/pnet/https.go +++ b/pnet/https.go @@ -47,7 +47,7 @@ const ( func (hp *Https) listenerThread() { defer hp.CloseErr() - defer parl.Recover(parl.Annotation(), nil, hp.SendErr) + defer parl.Recover("", nil, hp.SendErr) var didReadyWg bool defer func() { if !didReadyWg { diff --git a/pnet/socket-listener.go b/pnet/socket-listener.go index 549bbbdf..f35657bb 100644 --- a/pnet/socket-listener.go +++ b/pnet/socket-listener.go @@ -142,7 +142,7 @@ func (s *SocketListener[C]) AcceptConnections(handler func(C)) { } defer s.acceptWait.Done() // indicate accept thread exited defer s.connWait.Wait() // wait for connection goroutines - defer parl.Recover2(parl.Annotation(), nil, s.errCh.AddErrorProc) + defer parl.Recover2("", nil, s.errCh.AddErrorProc) s.handler = handler var err error @@ -267,7 +267,7 @@ func (s *SocketListener[C]) close(sendError bool) (didClose bool, err error) { // - invokeHandler recovers panics in handler function func (s *SocketListener[C]) invokeHandler(conn net.Conn) { defer s.connWait.Done() - defer parl.Recover2(parl.Annotation(), nil, s.errCh.AddErrorProc) + defer parl.Recover2("", nil, s.errCh.AddErrorProc) var c C var ok bool diff --git a/pnet/tcp.go b/pnet/tcp.go index 4d7e5bef..b44620d3 100644 --- a/pnet/tcp.go +++ b/pnet/tcp.go @@ -54,7 +54,7 @@ func (hp *Http) RunTLS() (errCh <-chan error) { func (hp *Http) run(errCh chan<- error, listener net.Listener) { defer close(errCh) - defer parl.Recover(parl.Annotation(), func(e error) { errCh <- e }) + defer parl.Recover("", func(e error) { errCh <- e }) if err := hp.Server.Serve(listener); err != nil { // blocking until Shutdown or Close if err != http.ErrServerClosed { errCh <- err diff --git a/pnet/udp.go b/pnet/udp.go index 600ae711..1a1e9fcd 100644 --- a/pnet/udp.go +++ b/pnet/udp.go @@ -79,7 +79,7 @@ func (udp *UDP) listenThread() { udp.StartingListen.Done() } }() - defer parl.Recover2(parl.Annotation(), nil, func(e error) { errCh <- e }) // capture panics + defer parl.Recover2("", nil, func(e error) { errCh <- e }) // capture panics // listen var netUDPConn *net.UDPConn // represents a network file descriptor diff --git a/pmaps/aggregate-priority.go b/pqs/aggregate-priority.go similarity index 72% rename from pmaps/aggregate-priority.go rename to pqs/aggregate-priority.go index 1f9d2182..7de1e453 100644 --- a/pmaps/aggregate-priority.go +++ b/pqs/aggregate-priority.go @@ -3,46 +3,47 @@ ISC License */ -package pmaps +// pqs provides legacy priority-queue implementation likely to be deprecated +package pqs import ( - "github.com/haraldrudell/parl/parli" + "github.com/haraldrudell/parl" "golang.org/x/exp/constraints" ) // AggregatePriority provides cached priority values and order function type AggregatePriority[V any, P constraints.Ordered] struct { assignedPriority AssignedPriority[V, P] - aggregator parli.Aggregator[V, P] + aggregator parl.Aggregator[V, P] } // NewAggregatePriority returns an object providing cached priority values and order function func NewAggregatePriority[V any, P constraints.Ordered]( value *V, index int, - aggregator parli.Aggregator[V, P], -) (aggregatePriority parli.AggregatePriority[V, P]) { + aggregator parl.Aggregator[V, P], +) (aggregatePriority parl.AggregatePriority[V, P]) { return &AggregatePriority[V, P]{ assignedPriority: *NewAssignedPriority(aggregator.Priority(), index, value), aggregator: aggregator, } } -var _ = ((parli.AggregatePriority[int, int])(&AggregatePriority[int, int]{})).Aggregator +var _ = ((parl.AggregatePriority[int, int])(&AggregatePriority[int, int]{})).Aggregator // Aggregator returns the object calculating values -func (a *AggregatePriority[V, P]) Aggregator() (aggregator parli.Aggregator[V, P]) { +func (a *AggregatePriority[V, P]) Aggregator() (aggregator parl.Aggregator[V, P]) { return a.aggregator } -var _ = ((parli.AggregatePriority[int, int])(&AggregatePriority[int, int]{})).Update +var _ = ((parl.AggregatePriority[int, int])(&AggregatePriority[int, int]{})).Update // Update caches the current priority from the aggregator func (a *AggregatePriority[V, P]) Update() { a.assignedPriority.SetPriority(a.aggregator.Priority()) } -var _ = ((parli.AggregatePriority[int, int])(&AggregatePriority[int, int]{})).CachedPriority +var _ = ((parl.AggregatePriority[int, int])(&AggregatePriority[int, int]{})).CachedPriority // Priority returns the effective cached priority func (a *AggregatePriority[V, P]) CachedPriority() (priority P) { @@ -58,7 +59,7 @@ func (a *AggregatePriority[V, P]) Index() (index int) { // - Cmp is a custom comparison function to be used with pslices and slices packages // - Cmp makes AggregatePriority ordered // - the Priority used is uncached value -func (x *AggregatePriority[V, P]) Cmp(a, b parli.AggregatePriority[V, P]) (result int) { +func (x *AggregatePriority[V, P]) Cmp(a, b parl.AggregatePriority[V, P]) (result int) { aPriority := a.CachedPriority() bPriority := b.CachedPriority() if aPriority > bPriority { // highest priority first diff --git a/pmaps/aggregating-priority-queue.go b/pqs/aggregating-priority-queue.go similarity index 75% rename from pmaps/aggregating-priority-queue.go rename to pqs/aggregating-priority-queue.go index c7c63728..d5bf6730 100644 --- a/pmaps/aggregating-priority-queue.go +++ b/pqs/aggregating-priority-queue.go @@ -3,28 +3,31 @@ ISC License */ -// Ranking is a pointer-identity-to-value map of updatable values traversable by rank. -// Ranking implements [parli.Ranking][V comparable, R constraints.Ordered]. -package pmaps +package pqs import ( + "github.com/haraldrudell/parl" "github.com/haraldrudell/parl/ids" "github.com/haraldrudell/parl/parli" "github.com/haraldrudell/parl/pslices" "golang.org/x/exp/constraints" ) -// AggregatingPriorityQueue implements a priority queue using cached priorities from aggregators -// - identity is the pointer value to each aggregator of type V +// AggregatingPriorityQueue implements a priority queue using +// cached priority from aggregators +// - item type is parl.AggregatePriority that can cache priorities +// - item identity is the pointer value to each aggregator of type V +// - the queue is periodically cleared, then regenerated by updating all items +// - the return queue length is configurable to be 1 or longer // - P is the type used for priority, ordered highest first // - insertion order is used for equal priorities, order lowest/earliest first type AggregatingPriorityQueue[V any, P constraints.Ordered] struct { // queue is a list of queue nodes ordered by rank - // - element is parli.AggregatePriority: pointer to AggregatePriority + // - element is parl.AggregatePriority: pointer to AggregatePriority // - AggregatePriority holds a cached priority value and the aggregator with running totals - queue parli.Ordered[parli.AggregatePriority[V, P]] + queue parli.Ordered[parl.AggregatePriority[V, P]] // m provides O(1) access to priority data-nodes via the value-pointer - m map[*V]parli.AggregatePriority[V, P] + m map[*V]parl.AggregatePriority[V, P] // indexGenerator provides IDs for insertion-ordering indexGenerator ids.UniqueIDint // non-zero for limiting queue length @@ -36,7 +39,7 @@ var _ AggregatePriority[int, int] // NewAggregatingPriorityQueue returns a map of updatable values traversable by rank func NewAggregatingPriorityQueue[V any, P constraints.Ordered]( maxQueueLength ...int, -) (priorityQueue parli.AggregatingPriorityQueue[V, P]) { +) (priorityQueue parl.AggregatingPriorityQueue[V, P]) { var maxQueueLength0 int if len(maxQueueLength) > 0 { maxQueueLength0 = maxQueueLength[0] @@ -46,15 +49,15 @@ func NewAggregatingPriorityQueue[V any, P constraints.Ordered]( } var a *AggregatePriority[V, P] return &AggregatingPriorityQueue[V, P]{ - m: map[*V]parli.AggregatePriority[V, P]{}, + m: map[*V]parl.AggregatePriority[V, P]{}, queue: pslices.NewOrderedAny(a.Cmp), maxQueueLength: maxQueueLength0, } } // Get retrieves a the value container with running totals associated with the identity valuep -func (a *AggregatingPriorityQueue[V, P]) Get(valuep *V) (aggregator parli.Aggregator[V, P], ok bool) { - var nodep parli.AggregatePriority[V, P] +func (a *AggregatingPriorityQueue[V, P]) Get(valuep *V) (aggregator parl.Aggregator[V, P], ok bool) { + var nodep parl.AggregatePriority[V, P] if nodep, ok = a.m[valuep]; ok { aggregator = nodep.Aggregator() } @@ -63,7 +66,7 @@ func (a *AggregatingPriorityQueue[V, P]) Get(valuep *V) (aggregator parli.Aggreg // Put stores a new value container associated with valuep // - the valuep is assumed to not have a node in the queue -func (a *AggregatingPriorityQueue[V, P]) Put(valuep *V, aggregator parli.Aggregator[V, P]) { +func (a *AggregatingPriorityQueue[V, P]) Put(valuep *V, aggregator parl.Aggregator[V, P]) { // create aggregatePriority with current priority from aggregator aggregatePriority := NewAggregatePriority( @@ -80,7 +83,7 @@ func (a *AggregatingPriorityQueue[V, P]) Put(valuep *V, aggregator parli.Aggrega // Update re-prioritizes a value func (a *AggregatingPriorityQueue[V, P]) Update(valuep *V) { - var aggregatePriority parli.AggregatePriority[V, P] + var aggregatePriority parl.AggregatePriority[V, P] var ok bool if aggregatePriority, ok = a.m[valuep]; !ok { return // value priority does not exist return @@ -93,7 +96,7 @@ func (a *AggregatingPriorityQueue[V, P]) Update(valuep *V) { a.insert(aggregatePriority) } -var _ = ((parli.AggregatingPriorityQueue[int, int])(&AggregatingPriorityQueue[int, int]{})).Clear +var _ = ((parl.AggregatingPriorityQueue[int, int])(&AggregatingPriorityQueue[int, int]{})).Clear // Clear empties the priority queue. The hashmap is left intact. func (a *AggregatingPriorityQueue[V, P]) Clear() { @@ -101,11 +104,11 @@ func (a *AggregatingPriorityQueue[V, P]) Clear() { } // List returns the first n or default all values by pirority -func (a *AggregatingPriorityQueue[V, P]) List(n ...int) (aggregatorQueue []parli.AggregatePriority[V, P]) { +func (a *AggregatingPriorityQueue[V, P]) List(n ...int) (aggregatorQueue []parl.AggregatePriority[V, P]) { return a.queue.List(n...) } -func (a *AggregatingPriorityQueue[V, P]) insert(aggregatePriority parli.AggregatePriority[V, P]) { +func (a *AggregatingPriorityQueue[V, P]) insert(aggregatePriority parl.AggregatePriority[V, P]) { // enforce max length if a.maxQueueLength > 0 && a.queue.Length() == a.maxQueueLength { diff --git a/pmaps/assigned-priority.go b/pqs/assigned-priority.go similarity index 98% rename from pmaps/assigned-priority.go rename to pqs/assigned-priority.go index a9bad372..35d8f4cb 100644 --- a/pmaps/assigned-priority.go +++ b/pqs/assigned-priority.go @@ -3,7 +3,7 @@ ISC License */ -package pmaps +package pqs import ( "golang.org/x/exp/constraints" diff --git a/pmaps/priority-queue-thread-safe.go b/pqs/priority-queue-thread-safe.go similarity index 86% rename from pmaps/priority-queue-thread-safe.go rename to pqs/priority-queue-thread-safe.go index b7f81a8d..617630b9 100644 --- a/pmaps/priority-queue-thread-safe.go +++ b/pqs/priority-queue-thread-safe.go @@ -4,18 +4,18 @@ ISC License */ // RankingThreadSafe is a thread-safe pointer-identity-to-value map of updatable values traversable by rank. -// RankingThreadSafe implements [parli.Ranking][V comparable, R constraints.Ordered]. -package pmaps +// RankingThreadSafe implements [parl.Ranking][V comparable, R constraints.Ordered]. +package pqs import ( "sync" - "github.com/haraldrudell/parl/parli" + "github.com/haraldrudell/parl" "golang.org/x/exp/constraints" ) // PriorityQueueThreadSafe is a thread-safe pointer-identity-to-value map of updatable values traversable by rank. -// PriorityQueueThreadSafe implements [parli.Ranking][V comparable, R constraints.Ordered]. +// PriorityQueueThreadSafe implements [parl.Ranking][V comparable, R constraints.Ordered]. // - V is a value reference composite type that is comparable, ie. not slice map function. // Preferrably, V is interface or pointer to struct type. // - P is an ordered type such as int floating-point string, used to rank the V values @@ -27,13 +27,13 @@ import ( // - values can have the same rank. If they do, equal rank is provided in insertion order type PriorityQueueThreadSafe[V any, P constraints.Ordered] struct { lock sync.RWMutex - parli.PriorityQueue[V, P] + parl.PriorityQueue[V, P] } // NewRanking returns a thread-safe map of updatable values traversable by rank func NewPriorityQueueThreadSafe[V any, P constraints.Ordered]( ranker func(value *V) (rank P), -) (o1 parli.PriorityQueue[V, P]) { +) (o1 parl.PriorityQueue[V, P]) { return &PriorityQueueThreadSafe[V, P]{ PriorityQueue: NewPriorityQueue(ranker), } diff --git a/pmaps/priority-queue-thread-safe_test.go b/pqs/priority-queue-thread-safe_test.go similarity index 95% rename from pmaps/priority-queue-thread-safe_test.go rename to pqs/priority-queue-thread-safe_test.go index 30b8713e..24da65ff 100644 --- a/pmaps/priority-queue-thread-safe_test.go +++ b/pqs/priority-queue-thread-safe_test.go @@ -3,13 +3,13 @@ ISC License */ -package pmaps +package pqs import ( "strconv" "testing" - "github.com/haraldrudell/parl/parli" + "github.com/haraldrudell/parl" ) func TestNewOrderedThreadSafe(t *testing.T) { @@ -28,7 +28,7 @@ func TestNewOrderedThreadSafe(t *testing.T) { exp1 := []*entity{&entity2, &entity1} expLength := 2 - var ranking parli.PriorityQueue[entity, int] + var ranking parl.PriorityQueue[entity, int] var pmapsRankingThreadSafe *PriorityQueueThreadSafe[entity, int] var pmapsRanking *PriorityQueue[entity, int] var ok bool diff --git a/pmaps/priority-queue.go b/pqs/priority-queue.go similarity index 92% rename from pmaps/priority-queue.go rename to pqs/priority-queue.go index a267636e..f4e4e019 100644 --- a/pmaps/priority-queue.go +++ b/pqs/priority-queue.go @@ -4,10 +4,11 @@ ISC License */ // Ranking is a pointer-identity-to-value map of updatable values traversable by rank. -// Ranking implements [parli.Ranking][V comparable, R constraints.Ordered]. -package pmaps +// Ranking implements [parl.Ranking][V comparable, R constraints.Ordered]. +package pqs import ( + "github.com/haraldrudell/parl" "github.com/haraldrudell/parl/parli" "github.com/haraldrudell/parl/perrors" "github.com/haraldrudell/parl/pslices" @@ -15,7 +16,7 @@ import ( ) // PriorityQueue is a pointer-identity-to-value map of updatable values traversable by rank. -// PriorityQueue implements [parli.PriorityQueue][V comparable, R constraints.Ordered]. +// PriorityQueue implements [parl.PriorityQueue][V comparable, R constraints.Ordered]. // - V is a value reference composite type that is comparable, ie. not slice map function. // Preferrably, V is interface or pointer to struct type. // - R is an ordered type such as int floating-point string, used to rank the V values @@ -37,7 +38,7 @@ type PriorityQueue[V any, P constraints.Ordered] struct { // NewPriorityQueue returns a map of updatable values traversable by rank func NewPriorityQueue[V any, P constraints.Ordered]( priorityFunc func(value *V) (priority P), -) (priorityQueue parli.PriorityQueue[V, P]) { +) (priorityQueue parl.PriorityQueue[V, P]) { if priorityFunc == nil { perrors.NewPF("ranker cannot be nil") } diff --git a/pmaps/priority-queue_test.go b/pqs/priority-queue_test.go similarity index 94% rename from pmaps/priority-queue_test.go rename to pqs/priority-queue_test.go index d61247c2..602d23c1 100644 --- a/pmaps/priority-queue_test.go +++ b/pqs/priority-queue_test.go @@ -3,14 +3,13 @@ ISC License */ -// Ranking is a map of updatable values traversable by rank -package pmaps +package pqs import ( "strconv" "testing" - "github.com/haraldrudell/parl/parli" + "github.com/haraldrudell/parl" ) func TestNewRanking(t *testing.T) { @@ -35,7 +34,7 @@ func TestNewRanking(t *testing.T) { exp3 := []*entity{&entity1} expLength := 3 - var ranking parli.PriorityQueue[entity, int] + var ranking parl.PriorityQueue[entity, int] var pmapsRanking *PriorityQueue[entity, int] var ok bool var rankList []*entity diff --git a/promise.go b/promise.go index 8bc96cab..30b694cf 100644 --- a/promise.go +++ b/promise.go @@ -49,7 +49,7 @@ func NewPromise[T any](resolver TFunc[T], g0 Go) (promise *Promise[T]) { func promiseThread[T any](resolver TFunc[T], future *Future[TResult[T]], g0 Go) { var err error defer g0.Done(&err) - defer Recover(Annotation(), &err, NoOnError) + defer PanicToErr(&err) var promiseValue TResult[T] defer future.End(&promiseValue, &promiseValue.Err) diff --git a/pterm/go.mod b/pterm/go.mod index fa15bbcc..189d0b10 100644 --- a/pterm/go.mod +++ b/pterm/go.mod @@ -5,7 +5,7 @@ go 1.21 replace github.com/haraldrudell/parl => ../../parl require ( - github.com/haraldrudell/parl v0.4.115 + github.com/haraldrudell/parl v0.4.116 golang.org/x/term v0.13.0 ) diff --git a/recover-da.go b/recover-da.go new file mode 100644 index 00000000..2139d591 --- /dev/null +++ b/recover-da.go @@ -0,0 +1,48 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package parl + +import "github.com/haraldrudell/parl/pruntime" + +const ( + // counts the frames in [parl.A] + parlAFrames = 1 +) + +// DA is the value returned by a deferred code location function +type DA *pruntime.CodeLocation + +// A is a thunk returning a deferred code location +func A() DA { return pruntime.NewCodeLocation(parlAFrames) } + +// RecoverDA recovers panic using deferred annotation +// +// Usage: +// +// func someFunc() (err error) { +// defer parl.RecoverDA(func() parl.DA { return parl.A() }, &err, parl.NoOnError) +func RecoverDA(deferredLocation func() DA, errp *error, onError OnError) { + doRecovery("", deferredLocation, errp, onError, recover2OnErrrorOnce, noIsPanic, recover()) +} + +// RecoverErr recovers panic using deferred annotation +// +// Usage: +// +// func someFunc() (isPanic bool, err error) { +// defer parl.RecoverErr(func() parl.DA { return parl.A() }, &err, &isPanic) +func RecoverErr(deferredLocation func() DA, errp *error, isPanic ...*bool) { + var isPanicp *bool + if len(isPanic) > 0 { + isPanicp = isPanic[0] + } + doRecovery("", deferredLocation, errp, NoOnError, recover2OnErrrorOnce, isPanicp, recover()) +} + +// RecoverDA2 recovers panic using deferred annotation +func RecoverDA2(deferredLocation func() DA, errp *error, onError OnError) { + doRecovery("", deferredLocation, errp, onError, recover2OnErrrorMultiple, noIsPanic, recover()) +} diff --git a/recover-da_test.go b/recover-da_test.go new file mode 100644 index 00000000..0c8353d1 --- /dev/null +++ b/recover-da_test.go @@ -0,0 +1,67 @@ +/* +© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) +ISC License +*/ + +package parl + +import ( + "fmt" + "testing" + + "github.com/haraldrudell/parl/perrors" + "github.com/haraldrudell/parl/pruntime" +) + +func recoverDaPanic() (deferLocation, panicLocation *pruntime.CodeLocation, err error) { + deferLocation = pruntime.NewCodeLocation(0) + defer RecoverDA(func() DA { return A() }, &err, NoOnError) + + panickingFunction(&panicLocation) + return +} + +func panickingFunction(panicLine **pruntime.CodeLocation) { + if *panicLine = pruntime.NewCodeLocation(0); *(*int)(nil) != 0 { + _ = 1 + } +} + +func TestRecoverDA(t *testing.T) { + var expFormat = "panic detected in %s: “%s” at %s" + var expPanicMessage = func() (message string) { + defer func() { + message = recover().(error).Error() + }() + + _ = *(*int)(nil) + return + }() + + deferCL, panicCL, err := recoverDaPanic() + + // should be error + if err == nil { + t.Error("missing error") + t.FailNow() + } + + // defer location: parl.recoverDaPanic()-recover3_test.go:16 + t.Logf("defer location: %s", deferCL.Short()) + + // panic location: parl.panickingFunction()-recover3_test.go:24 + t.Logf("panic location: %s", panicCL.Short()) + + var expMessage = fmt.Sprintf(expFormat, + deferCL.PackFunc(), + expPanicMessage, + panicCL.Short(), + ) + var message = perrors.Short(err) + if message != expMessage { + t.Errorf("bad message:\n%q exp\n%q", + message, + expMessage, + ) + } +} diff --git a/recover-invocation-panic.go b/recover-invocation-panic.go index cb9ea699..b3dc7175 100644 --- a/recover-invocation-panic.go +++ b/recover-invocation-panic.go @@ -22,7 +22,7 @@ func RecoverInvocationPanic(fn func(), errp *error) { if errp == nil { panic(perrors.ErrorfPF("%w", ErrErrpNil)) } - defer Recover(Annotation(), errp, NoOnError) + defer PanicToErr(errp) fn() } @@ -34,7 +34,7 @@ func RecoverInvocationPanic(fn func(), errp *error) { var ErrErrpNil = NilValueError(errors.New("errp cannot be nil")) func RecoverInvocationPanicErr(fn func() (err error)) (isPanic bool, err error) { - defer Recover(Annotation(), &err, NoOnError) + defer PanicToErr(&err) isPanic = true err = fn() @@ -55,7 +55,7 @@ type TResult[T any] struct { func NewTResult[T any](tFunc TFunc[T]) (tResult *TResult[T]) { var t = TResult[T]{IsPanic: true} tResult = &t - defer Recover(Annotation(), &t.Err, NoOnError) + defer PanicToErr(&t.Err) t.Value, t.Err = tFunc() t.IsPanic = false diff --git a/recover.go b/recover.go index 8eb1d406..f3f8c255 100644 --- a/recover.go +++ b/recover.go @@ -7,54 +7,104 @@ package parl import ( "fmt" + "strings" "github.com/haraldrudell/parl/perrors" "github.com/haraldrudell/parl/pruntime" ) const ( - recAnnStackFrames = 1 - recRecStackFrames = 2 - recEnsureErrorFrames = 2 - // Recover() and Recover2() are deferred functions invoked on panic - // Because the functions are directly invoked by runtime panic code, - // there are no intermediate stack frames between Recover*() and runtime.panic*. - // therefore, the Recover stack frame must be included in the error stack frames - // recover2() + processRecover() + ensureError() == 3 - recProcessRecoverFrames = 3 + panicString = ": panic:" + recover2OnErrrorOnce = false + recover2OnErrrorMultiple = true ) -// Recover recovers from a panic invoking a function no more than once. -// If there is *errp does not hold an error and there is no panic, onError is not invoked. -// Otherwise, onError is invoked exactly once. -// *errp is updated with a possible panic. -func Recover(annotation string, errp *error, onError func(error)) { - recover2(annotation, errp, onError, false, recover()) +const ( + // counts the stack-frame in [parl.processRecover] + processRecoverFrames = 1 + // counts the stack-frame of [parl.doRecovery] and [parl.Recover] or [parl.Recover2] + // - but for panic detectpr to work, there must be one frame after + // runtime.gopanic, so remove one frame + doRecoveryFrames = 2 - 1 +) + +// OnError is a function that receives error values from an errp error pointer or a panic +type OnError func(err error) + +// NoOnError is used with Recover and Recover2 to silence the default error logging +func NoOnError(err error) {} + +var noIsPanic *bool + +// Recover recovers from panic invoking onError exactly once with an aggregate error value +// - annotation may be empty, errp and onError may be nil +// - errors in *errp and panic are aggregated into a single error value +// - if onError non-nil, the function is invoked once with the aggregate error +// - if onError nil, the aggregate error is logged to standard error +// - if onError is [Parl.NoOnErrror], logging is suppressed +// - if errp is non-nil, it is updated with the aggregate error +// - if annotation is empty, a default annotation is used for the immediate caller of Recover +func Recover(annotation string, errp *error, onError OnError) { + doRecovery(annotation, nil, errp, onError, recover2OnErrrorOnce, noIsPanic, recover()) } -// Recover2 recovers from a panic and may invoke onError multiple times. -// onError is invoked if there is an error at *errp and on a possible panic. -// *errp is updated with a possible panic. -func Recover2(annotation string, errp *error, onError func(error)) { - recover2(annotation, errp, onError, true, recover()) +// Recover2 recovers from panic invoking onError for any eror in *errp and any panic +// - annotation may be empty, errp and onError may be nil +// - if onError non-nil, the function is invoked with any error in *errp and any panic +// - if onError nil, the errors are logged to standard error +// - if onError is [Parl.NoOnErrror], logging is suppressed +// - if errp is non-nil, it is updated with an aggregate error +// - if annotation is empty, a default annotation is used for the immediate caller of Recover +func Recover2(annotation string, errp *error, onError OnError) { + doRecovery(annotation, nil, errp, onError, recover2OnErrrorMultiple, noIsPanic, recover()) } -func recover2(annotation string, errp *error, onError func(error), multiple bool, recoverValue interface{}) { - // ensure non-empty annotation - if annotation == "" { - annotation = pruntime.NewCodeLocation(recRecStackFrames).PackFunc() + ": panic:" - } +// doRecovery implements recovery ffor Recovery andd Recovery2 +func doRecovery(annotation string, deferredAnnotation func() DA, errp *error, onError OnError, multiple bool, isPanic *bool, recoverValue interface{}) { - // consume *errp + // build aggregate error in err var err error + + // if onError is to be invoked multiple times, + // and *errp contains an error, + // invoke onError or Log to standard error if errp != nil { if err = *errp; err != nil && multiple { - invokeOnError(onError, err) + invokeOnError(onError, err) // invokee onError or parl.Log } } // consume recover() - if e := processRecover(annotation, recoverValue); e != nil { + if recoverValue != nil { + if isPanic != nil { + *isPanic = true + } + if annotation == "" { + if deferredAnnotation != nil { + if da := deferredAnnotation(); da != nil { + var cL = (*pruntime.CodeLocation)(da) + // single wword package name + var packageName = cL.Package() + // recoverDaPanic.func1: hosting function name and a derived name for the function literal + var funcName = cL.FuncIdentifier() + // removed “.func1” suffix + if index := strings.LastIndex(funcName, "."); index != -1 { + funcName = funcName[:index] + } + annotation = fmt.Sprintf("panic detected in %s.%s:", + packageName, + funcName, + ) + } + } + if annotation == "" { + // default annotation cannot be obtained + // - the deferred Recover function is invoked directly from rutine, eg. runtime.gopanic + // - therefore, use fixed string + annotation = "recover from panic:" + } + } + e := processRecover(annotation, recoverValue, doRecoveryFrames) if multiple { invokeOnError(onError, e) } else { @@ -62,7 +112,9 @@ func recover2(annotation string, errp *error, onError func(error), multiple bool } } - // write back to *errp, do non-multiple invocation + // if err now contains any error + // - write bacxk to non-nil errp + // - if not multiple, invoke onErorr or Log the aggregate error if err != nil { if errp != nil && *errp != err { *errp = err @@ -73,95 +125,24 @@ func recover2(annotation string, errp *error, onError func(error), multiple bool } } -func invokeOnError(onError func(error), err error) { +// invokeOnError invokes an onError function or logs to standard error if onError is nil +func invokeOnError(onError OnError, err error) { if onError != nil { onError(err) - } else { - Log("Recover: %+v\n", err) + return } -} - -// NoOnError is used with Recover to silence the default error logging -func NoOnError(err error) {} - -// Annotation provides a default annotation [base package].[function]: "mypackage.MyFunc" -func Annotation() (annotation string) { - return fmt.Sprintf("Recover from panic in %s:", pruntime.NewCodeLocation(recAnnStackFrames).PackFunc()) + Log("Recover: %+v\n", err) } // processRecover ensures non-nil result to be error with Stack -func processRecover(annotation string, panicValue interface{}) (err error) { - if err = ensureError(panicValue, recProcessRecoverFrames); err == nil { - return // panicValue nil return: no error - } - - // annotate - if annotation != "" { - err = perrors.Errorf("%s \x27%w\x27", annotation, err) - } - return -} - -// AddToPanic ensures that a recover() value is an error or nil. -func EnsureError(panicValue interface{}) (err error) { - return ensureError(panicValue, recEnsureErrorFrames) -} - -func ensureError(panicValue interface{}, frames int) (err error) { - - if panicValue == nil { - return // no panic return - } - - // ensure value to be error - var ok bool - if err, ok = panicValue.(error); !ok { - err = fmt.Errorf("non-error value: %T %+[1]v", panicValue) - } - - // ensure stack trace - if !perrors.HasStack(err) { - err = perrors.Stackn(err, frames) - } - - return -} - -// AddToPanic takes a recover() value and adds it to additionalErr. -func AddToPanic(panicValue interface{}, additionalErr error) (err error) { - if err = EnsureError(panicValue); err == nil { - return additionalErr - } - if additionalErr == nil { - return +// - annotation is non-empty annotation indicating code loction or action +// - panicValue is non-nil value returned by built-in recover function +func processRecover(annotation string, panicValue interface{}, frames int) (err error) { + if frames < 0 { + frames = 0 } - return perrors.AppendError(err, additionalErr) -} - -// HandlePanic recovers from panic in fn returning error. -func HandlePanic(fn func()) (err error) { - defer Recover(Annotation(), &err, nil) - - fn() - return + return perrors.Errorf("%s “%w”", + annotation, + ensureError(panicValue, frames+processRecoverFrames), + ) } - -// HandleErrp recovers from a panic in fn storing at *errp. -// HandleErrp is deferable. -func HandleErrp(fn func(), errp *error) { - defer Recover(Annotation(), errp, nil) - - fn() -} - -// HandleParlError recovers from panic in fn invoking an error callback. -// HandleParlError is deferable -// storeError can be the thread-safe perrors.ParlError.AddErrorProc() -func HandleParlError(fn func(), storeError func(err error)) { - defer Recover(Annotation(), nil, storeError) - - fn() -} - -// perrors.ParlError.AddErrorProc can be used with HandleParlError -var _ func(err error) = (&perrors.ParlError{}).AddErrorProc diff --git a/recover_test.go b/recover_test.go index a06da9ec..a340a073 100644 --- a/recover_test.go +++ b/recover_test.go @@ -19,7 +19,7 @@ func TestResultPanic(t *testing.T) { // test recover from panic doFn("PANIC", // printed test name prefix "panic", // string used for panic - "Recover from panic in parl.parlPanic: 'non-error value: string panic'", // expected error text + "Recover from panic in parl.parlPanic: “non-error value: string panic”", // expected error text parlPanic, // the function to invoke t) // test for printing @@ -102,7 +102,7 @@ func doFn(testID, text, expectedErr string, fn func(text string, fne func(error) } func TestRecoverErrp(t *testing.T) { annotation := "annotation" - exp := annotation + " 'runtime error: invalid memory address or nil pointer dereference'" + exp := annotation + " “runtime error: invalid memory address or nil pointer dereference”" var err error @@ -160,43 +160,59 @@ func TestEnsureErrorNonErr(t *testing.T) { } func TestRecoverOnError(t *testing.T) { - annotation := "annotation-fixture" - exp := annotation + "\x20\x27runtime error: invalid memory address or nil pointer dereference\x27" - loc := "TestRecoverOnError" + // a provided annotationFixture value “annotation-fixture” + var annotationFixture = "annotation-fixture" + // ‘annotation-fixture “runtime error: invalid memory address or nil pointer dereference”’ + var expErrorMessage = annotationFixture + "\x20“runtime error: invalid memory address or nil pointer dereference”" + var cL *pruntime.CodeLocation + var errpNil *error var err error - // cause a panic that is recovered and stored in err + // cause a panic that is recovered and stored in err using onError function func() { - defer Recover(annotation, nil, func(e error) { err = e }) + defer Recover(annotationFixture, errpNil, func(e error) { err = e }) - var pt *int - _ = *pt + if cL = pruntime.NewCodeLocation(0); *(*int)(nil) != 0 { // nil dereference panic + _ = 1 + } }() - // examine err + // there should be a recovered error if err == nil { t.Error("Expected error missing") t.FailNow() - } else if err.Error() != exp { - t.Errorf("bad err.Error() after panic recovery: %q exp %q", err.Error(), exp) - } else { - // 221226 perrors.Short now detects panic! - // the line provided will be the exact line of the panic, - // not some frame from the recovering runtime. - // - errorglue.Indices: - // - recovery perrors.Short(err): - // "annotation-fixture 'runtime error: invalid memory address or nil pointer dereference' - // at parl.TestRecoverOnError.func1-recover_test.go:174" - short := perrors.Short(err) - if !strings.Contains(short, loc) { - t.Errorf("bad perrors.Short(err) after panic recovery: %q exp %q", short, loc) - } } + + // the error message should be correct + if err.Error() != expErrorMessage { + t.Errorf("bad err.Error() after panic recovery:\n%q exp\n%q", + err.Error(), + expErrorMessage, + ) + } + + // 221226 perrors.Short now detects panic! + // the line provided will be the exact line of the panic, + // not some frame from the recovering runtime. + // - errorglue.Indices: + // - recovery perrors.Short(err): + // "annotation-fixture 'runtime error: invalid memory address or nil pointer dereference' + // at parl.TestRecoverOnError.func1-recover_test.go:174" + var errorShort = perrors.Short(err) + var codeLineShort = cL.Short() + if !strings.HasSuffix(errorShort, codeLineShort) { + t.Errorf("perrors.Short(err) does not end with panic location:\n%q exp\n%q", + errorShort, + codeLineShort, + ) + } + t.Logf("recovery err.Error(): %q", err.Error()) t.Logf("recovery perrors.Short(err): %q", perrors.Short(err)) //t.Fail() } + func TestAnnotation(t *testing.T) { exp := Sprintf("Recover from panic in %s:", pruntime.NewCodeLocation(0).PackFunc()) actual := Annotation() diff --git a/serial-do-core.go b/serial-do-core.go index edc4495e..86426f43 100644 --- a/serial-do-core.go +++ b/serial-do-core.go @@ -86,7 +86,7 @@ func (sdo *SerialDoCore) Wait(at time.Time) { } func (sdo *SerialDoCore) doThread(at time.Time) { - defer Recover(Annotation(), nil, sdo.errFn) + defer Recover("", nil, sdo.errFn) for { sdo.invokeThunk(at) @@ -122,7 +122,7 @@ func (sdo *SerialDoCore) checkForMoreDo() (at time.Time) { } func (sdo *SerialDoCore) invokeThunk(at time.Time) { - defer Recover(Annotation(), nil, sdo.errFn) + defer Recover("", nil, sdo.errFn) sdo.thunk(at) } diff --git a/serial-do.go b/serial-do.go index 23a2b5f5..cdefdbe7 100644 --- a/serial-do.go +++ b/serial-do.go @@ -15,7 +15,6 @@ import ( ) // SerialDo serializes a thunk. -// type SerialDo struct { thunk func(at time.Time) eventReceiver func(event *SerialDoEvent) @@ -156,7 +155,7 @@ func (sdo *SerialDo) performDo(now time.Time) (typ SerialDoType, isPending bool) } func (sdo *SerialDo) invokeEventFn(typ SerialDoType, t time.Time) { - defer Recover(Annotation(), nil, sdo.errFn) + defer Recover("", nil, sdo.errFn) if sdo.eventReceiver == nil { return @@ -166,14 +165,14 @@ func (sdo *SerialDo) invokeEventFn(typ SerialDoType, t time.Time) { } func (sdo *SerialDo) invokeThunk(at time.Time) { - defer Recover(Annotation(), nil, sdo.errFn) + defer Recover("", nil, sdo.errFn) sdo.thunk(at) } func (sdo *SerialDo) doThread(at time.Time) { defer sdo.wg.Done() - defer Recover(Annotation(), nil, sdo.errFn) + defer Recover("", nil, sdo.errFn) for { sdo.invokeThunk(at) diff --git a/sets/elements.go b/sets/elements.go index 1d34ca57..867e119d 100644 --- a/sets/elements.go +++ b/sets/elements.go @@ -8,95 +8,157 @@ package sets import ( "fmt" "sync" + "sync/atomic" "github.com/haraldrudell/parl/iters" "github.com/haraldrudell/parl/perrors" ) -// type Elements[T comparable] []T +// Elements is an Iterator[Element[T]] that reads from concrete slice []E +// - E is input concrete type, likely *struct, that implements Element[T] +// - T is some comparable type that E produces, allowing for different +// instances of E to be distinguished from one another +// - Element[T] is a type used by sets, enumerations and bit-fields +// - the Elements iterator provides a sequence of interface-type elements +// based on a slice of concrete implementation-type values, +// without re-creating the slice. Go cannot do type assertions on a slice, +// only on individual values +type Elements[T comparable, E any] struct { + // elementSlice implements Cancel and delegateAction function + // - implements iterator for Element[T] + // - pointer since delegateAction is provided to delegate + *elementsAction[T, E] + // Delegator implements the value methods required by the [Iterator] interface + // - Next HasNext NextValue + // Same Has SameValue + // - the delegate provides DelegateAction[T] function + iters.Delegator[Element[T]] +} + +// elementsAction is an enclosed type implementing delegateAction function +type elementsAction[T comparable, E any] struct { + elements []E // the slice providing values + + // indicates that no further values can be returned + // - written behind publicsLock + noValuesAvailable atomic.Bool + + // publicsLock serializes invocations of iterator [ElementSlice.delegateAction] + publicsLock sync.Mutex + + // delegateAction -// ElementSlice ToDo -type ElementSlice[T comparable, E any] struct { - lock sync.Mutex - didNext bool // indicates whether any value has been sought - hasValue bool // indicates whether index has been verified to be valid - index int // index in slice, 0…len(slice) - slice []E + // didNext indicates that a Next operation has completed and that hasValue may be valid + // - behind publicsLock + didNext bool + // index in slice, 0…len(slice) + // - behind lock + index int } -// NewElements returns an iterator of interface-type sets.Element[T] based from a -// slice of non-interface-type Elements[T comparable]. +// NewElements returns an iterator of interface-type sets.Element[T] +// - elements is a slice of a concrete type, named E, that should implement +// sets.Element +// - at compile time, elements is slice of any: []any +// - based on a slice of non-interface-type Elements[T comparable]. func NewElements[T comparable, E any](elements []E) (iter iters.Iterator[Element[T]]) { - // runtime check if requyired type conversion works - var e *E - var a any = e - if _, ok := a.(Element[T]); !ok { - var returnElementTypeStr string + // runtime check if required type conversion works + var pointerToRuntimeE *E + var pointerToRuntimeEInterface any = pointerToRuntimeE + // type assertion of the runtime type *E to the interface type Element[T] + if _, ok := pointerToRuntimeEInterface.(Element[T]); !ok { + + // runtime type *E does not implement Element[T] + // - produce error message + + // string description of runtime type Element[T] + var runtimeTypeElementTStringDescription string var t *Element[T] - if returnElementTypeStr = fmt.Sprintf("%T", t); len(returnElementTypeStr) > 0 { - returnElementTypeStr = returnElementTypeStr[1:] + if runtimeTypeElementTStringDescription = fmt.Sprintf("%T", t); len(runtimeTypeElementTStringDescription) > 0 { + // drop leading * indicating pointer + runtimeTypeElementTStringDescription = runtimeTypeElementTStringDescription[1:] } - var eTypeStr string - if eTypeStr = fmt.Sprintf("%T", t); len(eTypeStr) > 0 { - eTypeStr = eTypeStr[1:] + + // string description of runtime type E + var runtimeTypeEStringDescription string + if runtimeTypeEStringDescription = fmt.Sprintf("%T", t); len(runtimeTypeEStringDescription) > 0 { + // delete the * indicating pointer + runtimeTypeEStringDescription = runtimeTypeEStringDescription[1:] } + panic(perrors.ErrorfPF("input type %s does not implement interface-type %s", - eTypeStr, returnElementTypeStr, + runtimeTypeEStringDescription, + runtimeTypeElementTStringDescription, )) } // create the iterator - slice := ElementSlice[T, E]{slice: elements} - return &iters.Delegator[Element[T]]{Delegate: &slice} + e := elementsAction[T, E]{elements: elements} + + return &Elements[T, E]{ + elementsAction: &e, + Delegator: *iters.NewDelegator(e.delegateAction), + } } -func (iter *ElementSlice[T, E]) Next(isSame iters.NextAction) (value Element[T], hasValue bool) { - iter.lock.Lock() - defer iter.lock.Unlock() +// delegateAction finds the next or the same value. Thread-safe +// - isSame == IsSame means first or same value should be returned +// - value is the sought value or the T type’s zero-value if no value exists +// - hasValue true means value was assigned a valid T value +func (i *elementsAction[T, E]) delegateAction(isSame iters.NextAction) (value Element[T], hasValue bool) { - // if next operation has not completed, we do not know if a value exist, - // and next operation must be completed. - // if next has completed and we seek the same value, next operation should not be done. - if !iter.didNext || isSame != iters.IsSame { + // fast outside-lock value-check + if i.noValuesAvailable.Load() { + return // no more values return + } - // find slice index to use - if iter.hasValue { - // if a value has been found and is valid, advance index. - // the final value for iter.index is len(iter.slice) - iter.index++ - } + i.publicsLock.Lock() + defer i.publicsLock.Unlock() - // check if the new index is within available slice values - // when iter.index has reached len(iter.slice), iter.hasValue is always false. - // when hasValue is false, iter.index will no longer be incremented. - iter.hasValue = iter.index < len(iter.slice) + // inside-lock value-check + if i.noValuesAvailable.Load() { + return // no more values return + } - // indicate that iter.hasValue is now valid - if !iter.didNext { - iter.didNext = true + // for IsSame operation the first value must be sought + // - therefore, if the first value has not been sought, seek it now or + // - if not IsSame operation, advance to the next value + if i.didNext { + if isSame == iters.IsNext { + // find slice index to use + // - advance index until final value len(i.slice) + if i.index < len(i.elements) { + i.index++ + } } + } else { + // note that first value has been sought + i.didNext = true } - // get the value if it is valid, otherwise zero-value - if hasValue = iter.hasValue; hasValue { - var ePointer *E = &iter.slice[iter.index] - var a any = ePointer - var ok bool - if value, ok = a.(Element[T]); !ok { - // this is checked in NewElements: should never happen - panic(perrors.ErrorfPF("type assertion failed: %T %T", ePointer, value)) - } + // check if the new index is within available slice values + if hasValue = i.index < len(i.elements); !hasValue { + i.noValuesAvailable.CompareAndSwap(false, true) + return // no values return } - return // value and hasValue indicates availability -} + // a is any but runtime type is *E, pointer to a concrete type, likely *struct + var ePointer any = &i.elements[i.index] + + // do type assertion of ePointer to interface Element[T] + var ok bool + if value, ok = ePointer.(Element[T]); !ok { + // this type assertion was checked by NewElements: should never happen + panic(perrors.ErrorfPF("type assertion failed: %T %T", ePointer, value)) + } -func (iter *ElementSlice[T, E]) Cancel() (err error) { - iter.lock.Lock() - defer iter.lock.Unlock() + return // hasValue true, value valid return +} - iter.hasValue = false // invalidate iter.value - iter.slice = nil // prevent any next operation +// Cancel release resources for this iterator. Thread-safe +// - not every iterator requires a Cancel invocation +func (i *elementsAction[T, E]) Cancel() (err error) { + i.noValuesAvailable.CompareAndSwap(false, true) return } diff --git a/slow-detector-thread.go b/slow-detector-thread.go index 36ad215c..f3b574e2 100644 --- a/slow-detector-thread.go +++ b/slow-detector-thread.go @@ -109,7 +109,7 @@ func (sdt *SlowDetectorThread) Stop(sdi *SlowDetectorInvocation) { func (sdt *SlowDetectorThread) thread(g0 Go) { var err error defer g0.Register("SlowDetectorThread" + goID().String()).Done(&err) - defer Recover(Annotation(), &err, NoOnError) + defer PanicToErr(&err) ticker := time.NewTicker(slowScanPeriod) defer ticker.Stop() diff --git a/sqliter/go.mod b/sqliter/go.mod index 7ad8b663..f3119819 100644 --- a/sqliter/go.mod +++ b/sqliter/go.mod @@ -8,7 +8,7 @@ replace github.com/haraldrudell/parl => ../../parl require ( github.com/google/uuid v1.4.0 - github.com/haraldrudell/parl v0.4.115 + github.com/haraldrudell/parl v0.4.116 modernc.org/sqlite v1.26.0 ) diff --git a/sqliter/stmt.go b/sqliter/stmt.go index 390bd90a..6865a347 100644 --- a/sqliter/stmt.go +++ b/sqliter/stmt.go @@ -316,7 +316,7 @@ func theThread( timer *time.Timer, cancelCh chan struct{}, ) { - defer parl.Recover(parl.Annotation(), nil, parl.Infallible) + defer parl.Recover("", nil, parl.Infallible) // 230718 there was a memory leak // - abandoned threads are stuck at the select diff --git a/test/recover-invocation-panic.go b/test/recover-invocation-panic.go index 037e72d8..133d7fcc 100644 --- a/test/recover-invocation-panic.go +++ b/test/recover-invocation-panic.go @@ -49,7 +49,7 @@ func RecoverInvocationPanic(fn func(), errp *error) { if errp == nil { panic(perrors.ErrorfPF("%w", ErrErrpNil)) } - defer Recover(Annotation(), errp, NoOnError) + defer Recover("", errp, NoOnError) fn() } diff --git a/watchfs/go.mod b/watchfs/go.mod index ab1f63b5..a5c0fa70 100644 --- a/watchfs/go.mod +++ b/watchfs/go.mod @@ -7,7 +7,7 @@ replace github.com/haraldrudell/parl => ../../parl require ( github.com/fsnotify/fsnotify v1.7.0 github.com/google/uuid v1.4.0 - github.com/haraldrudell/parl v0.4.115 + github.com/haraldrudell/parl v0.4.116 ) require ( diff --git a/watchfs/watcher.go b/watchfs/watcher.go index d21a4277..f92b1ab8 100644 --- a/watchfs/watcher.go +++ b/watchfs/watcher.go @@ -73,7 +73,7 @@ func (w *Watcher) Shutdown() { func (w *Watcher) errorThread() { w.wg.Done() - defer parl.Recover(parl.Annotation(), nil, parl.Infallible) + defer parl.Recover("", nil, parl.Infallible) errCh := w.watcher.Errors for { @@ -86,7 +86,7 @@ func (w *Watcher) errorThread() { } func (w *Watcher) eventThread() { w.wg.Done() - defer parl.Recover(parl.Annotation(), nil, w.errFn) + defer parl.Recover("", nil, w.errFn) events := w.watcher.Events for { @@ -112,7 +112,7 @@ func (w *Watcher) eventThread() { } func (w *Watcher) sendEvent(ev *WatchEvent) { - defer parl.Recover(parl.Annotation(), nil, w.errFn) + defer parl.Recover("", nil, w.errFn) w.eventFn(ev) } diff --git a/yamler/go.mod b/yamler/go.mod index 37a51474..9b19b055 100644 --- a/yamler/go.mod +++ b/yamler/go.mod @@ -7,7 +7,7 @@ replace github.com/haraldrudell/parl => ../../parl replace github.com/haraldrudell/parl/mains => ../mains require ( - github.com/haraldrudell/parl v0.4.115 + github.com/haraldrudell/parl v0.4.116 golang.org/x/exp v0.0.0-20231006140011-7918f672742d gopkg.in/yaml.v3 v3.0.1 )