-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add support for delayed start. Signed-off-by: Artur Souza <asouza.pro@gmail.com> * Remove unused struct for etcd mutex. Signed-off-by: Artur Souza <asouza.pro@gmail.com> * Add support for counter (repeats). Signed-off-by: Artur Souza <asouza.pro@gmail.com> * Add custom job delete logic. Signed-off-by: Artur Souza <asouza.pro@gmail.com> * Update rhythm/spec_test.go Co-authored-by: Josh van Leeuwen <me@joshvanl.dev> Signed-off-by: Artur Souza <asouza.pro@gmail.com> * Update counting/etcd.go Co-authored-by: Josh van Leeuwen <me@joshvanl.dev> Signed-off-by: Artur Souza <asouza.pro@gmail.com> * Addressed comments. Signed-off-by: Artur Souza <asouza.pro@gmail.com> * Simplify next calculation for constant delay. Signed-off-by: Artur Souza <asouza.pro@gmail.com> * Using fmt.Errorf instead of errors.Errorf Signed-off-by: Artur Souza <asouza.pro@gmail.com> --------- Signed-off-by: Artur Souza <asouza.pro@gmail.com> Co-authored-by: Josh van Leeuwen <me@joshvanl.dev>
- Loading branch information
1 parent
7059bf0
commit c0f44b7
Showing
17 changed files
with
704 additions
and
123 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/* | ||
Copyright (c) 2024 Diagrid Inc. | ||
Licensed under the MIT License. | ||
*/ | ||
|
||
package counting | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
"time" | ||
|
||
"github.com/diagridio/go-etcd-cron/partitioning" | ||
"github.com/google/uuid" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
etcdclientv3 "go.etcd.io/etcd/client/v3" | ||
) | ||
|
||
const defaultEtcdEndpoint = "127.0.0.1:2379" | ||
|
||
func TestCounterTTL(t *testing.T) { | ||
ctx := context.TODO() | ||
organizer := partitioning.NewOrganizer(randomNamespace(), partitioning.NoPartitioning()) | ||
etcdClient, err := etcdclientv3.New(etcdclientv3.Config{ | ||
Endpoints: []string{defaultEtcdEndpoint}, | ||
}) | ||
require.NoError(t, err) | ||
|
||
key := organizer.CounterPath(0, "count") | ||
// This counter will expire keys 1s after their next scheduled trigger. | ||
counter := NewEtcdCounter(etcdClient, key, time.Second) | ||
|
||
value, updated, err := counter.Add(ctx, 1, time.Now().Add(time.Second)) | ||
require.NoError(t, err) | ||
assert.True(t, updated) | ||
assert.Equal(t, 1, value) | ||
|
||
value, updated, err = counter.Add(ctx, 2, time.Now().Add(time.Second)) | ||
require.NoError(t, err) | ||
assert.True(t, updated) | ||
assert.Equal(t, 3, value) | ||
|
||
time.Sleep(time.Second) | ||
|
||
value, updated, err = counter.Add(ctx, -4, time.Now().Add(time.Second)) | ||
require.NoError(t, err) | ||
assert.True(t, updated) | ||
assert.Equal(t, -1, value) | ||
|
||
// Counter expires 1 second after the next scheduled trigger (in this test's config) | ||
time.Sleep(3 * time.Second) | ||
|
||
// Counter should have expired but the in-memory value continues. | ||
// Even if key is expired in the db, a new operation will set it again, with a new TTL. | ||
value, updated, err = counter.Add(ctx, 0, time.Now().Add(time.Second)) | ||
require.NoError(t, err) | ||
assert.True(t, updated) | ||
assert.Equal(t, -1, value) | ||
|
||
// Counter expires 1 second after the next scheduled trigger. | ||
time.Sleep(3 * time.Second) | ||
|
||
// A new instance will start from 0 since the db record is expired. | ||
counter = NewEtcdCounter(etcdClient, key, time.Second) | ||
value, updated, err = counter.Add(ctx, 0, time.Now().Add(time.Second)) | ||
require.NoError(t, err) | ||
assert.True(t, updated) | ||
assert.Equal(t, 0, value) | ||
} | ||
|
||
func randomNamespace() string { | ||
return uuid.New().String() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/* | ||
Copyright (c) 2024 Diagrid Inc. | ||
Licensed under the MIT License. | ||
*/ | ||
|
||
package counting | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strconv" | ||
"time" | ||
|
||
etcdclientv3 "go.etcd.io/etcd/client/v3" | ||
) | ||
|
||
type Counter interface { | ||
// Applies by the given delta (+ or -) and return the updated value. | ||
// Count has a ttl calculated using the next tick's time. | ||
// Returns (updated value, true if value was updated in memory, err if any error happened) | ||
// It is possible that the value is updated but an error occurred while trying to persist it. | ||
Add(context.Context, int, time.Time) (int, bool, error) | ||
} | ||
|
||
// It keeps a cache of the value and updates async. | ||
// It works assuming there cannot be two concurrent writes to the same key. | ||
// Concurrency is handled at the job level, which makes this work. | ||
type etcdcounter struct { | ||
etcdclient *etcdclientv3.Client | ||
key string | ||
|
||
loaded bool | ||
value int | ||
ttlOffset time.Duration | ||
} | ||
|
||
func NewEtcdCounter(c *etcdclientv3.Client, key string, ttlOffset time.Duration) Counter { | ||
return &etcdcounter{ | ||
etcdclient: c, | ||
key: key, | ||
ttlOffset: ttlOffset, | ||
} | ||
} | ||
|
||
func (c *etcdcounter) Add(ctx context.Context, delta int, next time.Time) (int, bool, error) { | ||
if !c.loaded { | ||
// First, load the key's value. | ||
res, err := c.etcdclient.KV.Get(ctx, c.key) | ||
if err != nil { | ||
return 0, false, err | ||
} | ||
if len(res.Kvs) == 0 { | ||
c.value = 0 | ||
c.loaded = true | ||
} else { | ||
if res.Kvs[0].Value == nil { | ||
return 0, false, fmt.Errorf("nil value for key %s", c.key) | ||
} | ||
if len(res.Kvs[0].Value) == 0 { | ||
return 0, false, fmt.Errorf("empty value for key %s", c.key) | ||
} | ||
|
||
c.value, err = strconv.Atoi(string(res.Kvs[0].Value)) | ||
if err != nil { | ||
return 0, false, err | ||
} | ||
} | ||
} | ||
|
||
c.value += delta | ||
// Create a lease | ||
ttl := time.Until(next.Add(c.ttlOffset)) | ||
lease, err := c.etcdclient.Grant(ctx, int64(ttl.Seconds())) | ||
if err != nil { | ||
return c.value, true, err | ||
} | ||
_, err = c.etcdclient.KV.Put(ctx, c.key, strconv.Itoa(c.value), etcdclientv3.WithLease(lease.ID)) | ||
return c.value, true, err | ||
} |
Oops, something went wrong.