Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: aperturerobotics/util
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.29.0
Choose a base ref
...
head repository: aperturerobotics/util
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.29.1
Choose a head ref
  • 3 commits
  • 10 files changed
  • 2 contributors

Commits on Mar 4, 2025

  1. chore(keyed): use modern ranges in tests

    Signed-off-by: Christian Stewart <christian@aperture.us>
    paralin committed Mar 4, 2025
    Copy the full SHA
    0ab6dc1 View commit details

Commits on Mar 24, 2025

  1. feat(routine): add state routine with a promise callback

    Signed-off-by: Christian Stewart <christian@aperture.us>
    paralin committed Mar 24, 2025
    Copy the full SHA
    d80b6da View commit details
  2. fix(deps): update all dependencies

    Signed-off-by: Christian Stewart <christian@aperture.us>
    renovate[bot] authored and paralin committed Mar 24, 2025
    Copy the full SHA
    a4853b6 View commit details
Showing with 772 additions and 750 deletions.
  1. +5 −5 .github/workflows/codeql-analysis.yml
  2. +2 −2 .github/workflows/tests.yml
  3. +4 −4 go.mod
  4. +10 −10 go.sum
  5. +32 −32 keyed/keyed_test.go
  6. +42 −0 routine/result.go
  7. +51 −0 routine/result_test.go
  8. +1 −1 routine/routine.go
  9. +5 −8 routine/state.go
  10. +620 −688 yarn.lock
10 changes: 5 additions & 5 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Setup Go ${{ matrix.go }}
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version: ${{ matrix.go }}

@@ -50,19 +50,19 @@ jobs:
${{ runner.os }}-go-
- name: Setup Node.JS ${{ matrix.node }}
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: ${{ matrix.node }}
cache: 'yarn'

- name: Initialize CodeQL
uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
with:
languages: ${{ matrix.language }}


- name: Autobuild
uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -23,12 +23,12 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Setup Go ${{ matrix.go }}
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version: ${{ matrix.go }}

- name: Setup Node.JS ${{ matrix.node }}
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: ${{ matrix.node }}
cache: 'yarn'
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -2,19 +2,19 @@ module github.com/aperturerobotics/util

go 1.24

toolchain go1.24.0
toolchain go1.24.1

require (
github.com/aperturerobotics/common v0.21.1 // latest
github.com/aperturerobotics/common v0.21.2 // latest
github.com/aperturerobotics/json-iterator-lite v1.0.1-0.20240713111131-be6bf89c3008 // indirect
github.com/aperturerobotics/protobuf-go-lite v0.8.0 // latest
github.com/aperturerobotics/protobuf-go-lite v0.8.1 // latest
)

require (
github.com/fsnotify/fsnotify v1.8.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
)

require golang.org/x/sys v0.13.0 // indirect
20 changes: 10 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
github.com/aperturerobotics/common v0.21.1 h1:n2kPPTVhTkNeJ5pDeH6u3tMuv07A53bek/TRMGNZIAs=
github.com/aperturerobotics/common v0.21.1/go.mod h1:FrecdNcsYvVS8RcWCR8FUkKFh+XmouFOYKHpBdMqqBA=
github.com/aperturerobotics/common v0.21.2 h1:fqnPL5Oovpd8nDaNBYGiD1UpZhcH/JfpsS8gt5iBDyA=
github.com/aperturerobotics/common v0.21.2/go.mod h1:FrecdNcsYvVS8RcWCR8FUkKFh+XmouFOYKHpBdMqqBA=
github.com/aperturerobotics/json-iterator-lite v1.0.1-0.20240713111131-be6bf89c3008 h1:So9JeziaWKx2Fw8sK4AUN/szqKtJ0jEMhS6bU4sHbxs=
github.com/aperturerobotics/json-iterator-lite v1.0.1-0.20240713111131-be6bf89c3008/go.mod h1:snaApCEDtrHHP6UWSLKiYNOZU9A5NyzccKenx9oZEzg=
github.com/aperturerobotics/protobuf-go-lite v0.8.0 h1:SoiTAVArmOrNTX31e6CC5Bem6HuOElg3YYNhp4AAPQc=
github.com/aperturerobotics/protobuf-go-lite v0.8.0/go.mod h1:y49wVEezRHg78uQ2OzLLZbtTTWuox+ChmaTuh6FLJW8=
github.com/aperturerobotics/protobuf-go-lite v0.8.1 h1:CcBvqWOSep4VF3pPp+ZLcYQwPQu2kGOvL3bbF71bzKE=
github.com/aperturerobotics/protobuf-go-lite v0.8.1/go.mod h1:6AD6TgBrC+aWprTirXrUASFvTbuIRAsuDLBNUthzcyA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -19,10 +19,10 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c=
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
64 changes: 32 additions & 32 deletions keyed/keyed_test.go
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@ func TestKeyed(t *testing.T) {

nsend := 101
keys := make([]string, nsend)
for i := 0; i < nsend; i++ {
for i := range nsend {
key := "routine-" + strconv.Itoa(i)
keys[i] = key
}
@@ -96,7 +96,7 @@ func TestKeyed_WithDelay(t *testing.T) {
var called, canceled atomic.Bool
calledCh := make(chan struct{})
canceledCh := make(chan struct{})

k := NewKeyed(
func(key string) (Routine, *testData) {
return func(ctx context.Context) error {
@@ -120,10 +120,10 @@ func TestKeyed_WithDelay(t *testing.T) {
if !called.Load() || canceled.Load() {
t.Fail()
}

// Remove the key, but it should still be running due to delay
_ = k.RemoveKey("test")

// Create a timer to check if the routine is still running after some time
// This is one case where we need a timer since we're testing time-based behavior
timer := time.NewTimer(time.Millisecond * 100)
@@ -133,7 +133,7 @@ func TestKeyed_WithDelay(t *testing.T) {
case <-timer.C:
// Expected - routine should still be running
}

// Now wait for cancellation to happen after the delay
<-canceledCh
if !called.Load() || !canceled.Load() {
@@ -151,13 +151,13 @@ func TestKeyed_WithDelay(t *testing.T) {
if !called.Load() || canceled.Load() {
t.Fail()
}

// Remove the key, but it should still be running due to delay
_ = k.RemoveKey("test")

// Set the key again before the delay expires
k.SetKey("test", false)

// Verify the routine is still running and wasn't canceled
timer.Reset(time.Millisecond * 200)
select {
@@ -166,7 +166,7 @@ func TestKeyed_WithDelay(t *testing.T) {
case <-timer.C:
// Expected - routine should still be running
}

if !called.Load() || canceled.Load() {
t.Fail()
}
@@ -264,17 +264,17 @@ func TestKeyedRefCount(t *testing.T) {

// Create a channel to wait for the routine to start
startCh := make(chan struct{})

// Wait for the routine to start
for i := 0; i < 100; i++ {
for range 100 {
if startCount.Load() == 1 {
close(startCh)
break
}
// Small yield to allow other goroutines to run
runtime.Gosched()
}

<-startCh
if startCount.Load() != 1 {
t.Fatal("routine should have started once")
@@ -285,7 +285,7 @@ func TestKeyedRefCount(t *testing.T) {

// Release one reference, routine should still be running
ref1.Release()

// Verify state hasn't changed
if startCount.Load() != 1 {
t.Fatal("routine should have started once")
@@ -296,17 +296,17 @@ func TestKeyedRefCount(t *testing.T) {

// Release the second reference, routine should stop
ref2.Release()

// Wait for the routine to stop
stopCh := make(chan struct{})
for i := 0; i < 100; i++ {
for range 100 {
if stopCount.Load() == 1 {
close(stopCh)
break
}
runtime.Gosched()
}

<-stopCh
if startCount.Load() != 1 {
t.Fatal("routine should have started once")
@@ -317,17 +317,17 @@ func TestKeyedRefCount(t *testing.T) {

// Add a reference again, routine should restart
ref3, _, _ := k.AddKeyRef("test-key")

// Wait for the routine to start again
startCh2 := make(chan struct{})
for i := 0; i < 100; i++ {
for range 100 {
if startCount.Load() == 2 {
close(startCh2)
break
}
runtime.Gosched()
}

<-startCh2
if startCount.Load() != 2 {
t.Fatal("routine should have started twice")
@@ -338,17 +338,17 @@ func TestKeyedRefCount(t *testing.T) {

// Remove the key directly, should stop the routine
k.RemoveKey("test-key")

// Wait for the routine to stop again
stopCh2 := make(chan struct{})
for i := 0; i < 100; i++ {
for range 100 {
if stopCount.Load() == 2 {
close(stopCh2)
break
}
runtime.Gosched()
}

<-stopCh2
if startCount.Load() != 2 {
t.Fatal("routine should have started twice")
@@ -387,7 +387,7 @@ func TestExitCallbacks(t *testing.T) {
}, &testData{}
},
WithExitLogger[string, *testData](le),
WithExitCb[string, *testData](exitCb),
WithExitCb(exitCb),
)

k.SetContext(ctx, true)
@@ -463,7 +463,7 @@ func TestRestartReset(t *testing.T) {
runtime.Gosched()
}
}()

existed, restarted := k.RestartRoutine("test-key")
if !existed || !restarted {
t.Fatal("restart should have succeeded")
@@ -492,7 +492,7 @@ func TestRestartReset(t *testing.T) {
runtime.Gosched()
}
}()

existed, reset := k.ResetRoutine("test-key")
if !existed || !reset {
t.Fatal("reset should have succeeded")
@@ -521,7 +521,7 @@ func TestRestartReset(t *testing.T) {
runtime.Gosched()
}
}()

existed, reset = k.ResetRoutine("test-key", func(k string, v *testData) bool {
return v.value == "test-key-2"
})
@@ -552,7 +552,7 @@ func TestRestartReset(t *testing.T) {
runtime.Gosched()
}
}()

resetCount2, totalCount := k.ResetAllRoutines()
if resetCount2 != 1 || totalCount != 1 {
t.Fatal("reset all should have reset one routine")
@@ -608,7 +608,7 @@ func TestContextCancellation(t *testing.T) {

// Wait for callback to be called
<-exitCh

mu.Lock()
if len(exitErrors) != 1 {
t.Fatal("should have one exit error")
@@ -621,7 +621,7 @@ func TestContextCancellation(t *testing.T) {
// Set a new context
newCtx := context.Background()
k.SetContext(newCtx, true)

// Create a channel for the second exit
exitCh2 := make(chan struct{})
var exitWg sync.WaitGroup
@@ -639,14 +639,14 @@ func TestContextCancellation(t *testing.T) {
runtime.Gosched()
}
}()

// Cancel the key
k.RemoveKey("test-key")

// Wait for callback to be called again
<-exitCh2
exitWg.Wait()

mu.Lock()
if len(exitErrors) != 2 {
t.Fatal("should have two exit errors")
42 changes: 42 additions & 0 deletions routine/result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package routine

import (
"context"

"github.com/aperturerobotics/util/promise"
)

// StateResultRoutine is a function called as a goroutine with a state parameter.
// If the state changes, ctx will be canceled and the function restarted.
// If nil is returned as first return value, exits cleanly permanently.
// If an error is returned, can still be restarted later.
// The second return value is the result value.
// This is a wrapper around StateRoutine that also returns a result.
type StateResultRoutine[T comparable, R any] func(ctx context.Context, st T) (R, error)

// NewStateResultRoutine constructs a new StateRoutine from a StateResultRoutine.
// The routine stores the result in the PromiseContainer.
func NewStateResultRoutine[T comparable, R any](srr StateResultRoutine[T, R]) (StateRoutine[T], *promise.PromiseContainer[R]) {
ctr := promise.NewPromiseContainer[R]()
return NewStateResultRoutineWithPromiseContainer(srr, ctr), ctr
}

// NewStateResultRoutineWithPromiseContainer constructs a new StateRoutine from a StateResultRoutine.
// The routine stores the result in the provided PromiseContainer.
func NewStateResultRoutineWithPromiseContainer[T comparable, R any](
srr StateResultRoutine[T, R],
resultCtr *promise.PromiseContainer[R],
) StateRoutine[T] {
return func(ctx context.Context, st T) error {
prom := promise.NewPromise[R]()
resultCtr.SetPromise(prom)

result, err := srr(ctx, st)
if ctx.Err() != nil {
return context.Canceled
}
prom.SetResult(result, err)

return err
}
}
Loading