improved mem & cpu consumption
This commit is contained in:
38
async.go
38
async.go
@@ -3,6 +3,7 @@ package gubgub
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"iter"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,16 +84,7 @@ func (t *AsyncTopic[T]) run() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
keepers := make([]Subscriber[T], 0, len(subscribers))
|
subscribers = sequentialDelivery(msg, subscribers)
|
||||||
|
|
||||||
for _, callback := range subscribers {
|
|
||||||
keep := callback(msg)
|
|
||||||
if keep {
|
|
||||||
keepers = append(keepers, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribers = keepers
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,6 +123,32 @@ func (t *AsyncTopic[T]) Subscribe(fn Subscriber[T]) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Feed allows you to for/range to consume future published messages. The supporting subscriber will eventually be discarded after you exit the for loop.
|
||||||
|
func (t *AsyncTopic[T]) Feed() iter.Seq[T] {
|
||||||
|
feed := make(chan T, 1)
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
t.Subscribe(func(msg T) bool {
|
||||||
|
select {
|
||||||
|
case feed <- msg:
|
||||||
|
return true
|
||||||
|
case <-done:
|
||||||
|
close(feed)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return func(yield func(T) bool) {
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
for msg := range feed {
|
||||||
|
if !yield(msg) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type AsyncTopicOptions struct {
|
type AsyncTopicOptions struct {
|
||||||
onClose func()
|
onClose func()
|
||||||
onSubscribe func(count int)
|
onSubscribe func(count int)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkAsyncTopic_Publish(b *testing.B) {
|
func BenchmarkAsyncTopic_Publish(b *testing.B) {
|
||||||
for _, tc := range benchTestCase {
|
for _, tc := range publishCases {
|
||||||
b.Run(tc.Name, func(b *testing.B) {
|
b.Run(tc.Name, func(b *testing.B) {
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package gubgub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -55,10 +56,13 @@ func TestAsyncTopic_MultiPublishersMultiSubscribers(t *testing.T) {
|
|||||||
msgCount = pubCount * 100 // total messages to publish (delivered to EACH subscriber)
|
msgCount = pubCount * 100 // total messages to publish (delivered to EACH subscriber)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
subscribersReady := make(chan struct{}, 1)
|
subscribersReady := make(chan struct{}, 1)
|
||||||
defer close(subscribersReady)
|
defer close(subscribersReady)
|
||||||
|
|
||||||
topic := NewAsyncTopic[int](context.Background(), WithOnSubscribe(func(count int) {
|
topic := NewAsyncTopic[int](ctx, WithOnSubscribe(func(count int) {
|
||||||
if count == subCount {
|
if count == subCount {
|
||||||
subscribersReady <- struct{}{}
|
subscribersReady <- struct{}{}
|
||||||
}
|
}
|
||||||
@@ -245,6 +249,50 @@ func TestAsyncTopic_AllPublishedBeforeClosedAreDeliveredAfterClosed(t *testing.T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAsyncTopic_Feed(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
subscriberReady := make(chan struct{}, 1)
|
||||||
|
defer close(subscriberReady)
|
||||||
|
|
||||||
|
topic := NewAsyncTopic[int](ctx,
|
||||||
|
WithOnSubscribe(func(count int) {
|
||||||
|
subscriberReady <- struct{}{}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
seen := make(map[int]struct{})
|
||||||
|
for i := range topic.Feed() {
|
||||||
|
seen[i] = struct{}{}
|
||||||
|
if len(seen) >= 9 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-subscriberReady
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for i := range 10 {
|
||||||
|
topic.Publish(i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
func testTimer(t testing.TB, d time.Duration) *time.Timer {
|
func testTimer(t testing.TB, d time.Duration) *time.Timer {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package gubgub
|
package gubgub
|
||||||
|
|
||||||
|
import "sync/atomic"
|
||||||
|
|
||||||
type benchSubscriberSetup struct {
|
type benchSubscriberSetup struct {
|
||||||
Name string
|
Name string
|
||||||
Count int
|
Count int
|
||||||
Subscriber Subscriber[int]
|
Subscriber Subscriber[int]
|
||||||
}
|
}
|
||||||
|
|
||||||
var benchTestCase = []benchSubscriberSetup{
|
var publishCases = []benchSubscriberSetup{
|
||||||
{
|
{
|
||||||
Name: "10 NoOp Subscribers",
|
Name: "10 NoOp Subscribers",
|
||||||
Count: 10,
|
Count: 10,
|
||||||
@@ -45,3 +47,44 @@ func Slow(int) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var deliveryCases = []benchSubscriberSetup{
|
||||||
|
{
|
||||||
|
Name: "10K Subscribers 0 unsubscribe",
|
||||||
|
Count: 10000,
|
||||||
|
Subscriber: NoOp[int](),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "100K Subscribers 0 unsubscribe",
|
||||||
|
Count: 100000,
|
||||||
|
Subscriber: NoOp[int](),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "10K Subscribers 1% unsubscribe",
|
||||||
|
Count: 10000,
|
||||||
|
Subscriber: Quiter(100),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "100K Subscribers 1% unsubscribe",
|
||||||
|
Count: 100000,
|
||||||
|
Subscriber: Quiter(100),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "10K Subscribers 2% unsubscribe",
|
||||||
|
Count: 10000,
|
||||||
|
Subscriber: Quiter(50),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "100K Subscribers 2% unsubscribe",
|
||||||
|
Count: 100000,
|
||||||
|
Subscriber: Quiter(50),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quiter returns a subscriber that unsubscribes nth calls.
|
||||||
|
func Quiter(nth int64) func(_ int) bool {
|
||||||
|
var c atomic.Int64
|
||||||
|
return func(_ int) bool {
|
||||||
|
return c.Add(1)%nth != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
27
delivery.go
Normal file
27
delivery.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package gubgub
|
||||||
|
|
||||||
|
// sequentialDelivery delivers a message to each subscriber sequentially. For performance reasons
|
||||||
|
// this might mutate the subscribers slice inplace. Please overwrite it with the result of this
|
||||||
|
// call.
|
||||||
|
func sequentialDelivery[T any](msg T, subscribers []Subscriber[T]) []Subscriber[T] {
|
||||||
|
last := len(subscribers) - 1
|
||||||
|
next := 0
|
||||||
|
|
||||||
|
for next <= last {
|
||||||
|
if !subscribers[next](msg) {
|
||||||
|
for last > next && !subscribers[last](msg) {
|
||||||
|
last--
|
||||||
|
}
|
||||||
|
|
||||||
|
if last <= next {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribers[next] = subscribers[last]
|
||||||
|
last--
|
||||||
|
}
|
||||||
|
next++
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscribers[:next]
|
||||||
|
}
|
||||||
28
delivery_bench_test.go
Normal file
28
delivery_bench_test.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package gubgub
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func BenchmarkSequentialDelivery(b *testing.B) {
|
||||||
|
for _, tc := range deliveryCases {
|
||||||
|
b.Run(tc.Name, func(b *testing.B) {
|
||||||
|
subscribers := make([]Subscriber[int], 0, tc.Count)
|
||||||
|
|
||||||
|
for range tc.Count {
|
||||||
|
subscribers = append(subscribers, tc.Subscriber)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := range b.N {
|
||||||
|
b.StartTimer()
|
||||||
|
subscribers = sequentialDelivery(i, subscribers)
|
||||||
|
b.StopTimer()
|
||||||
|
|
||||||
|
// replenish subscribers
|
||||||
|
for len(subscribers) < tc.Count {
|
||||||
|
subscribers = append(subscribers, tc.Subscriber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
41
delivery_test.go
Normal file
41
delivery_test.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package gubgub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSequentialDelivery(t *testing.T) {
|
||||||
|
const testMsg = 9786
|
||||||
|
|
||||||
|
feedback := make([]int, 0, 3)
|
||||||
|
|
||||||
|
subscribers := []Subscriber[int]{
|
||||||
|
func(i int) bool {
|
||||||
|
assert.Equalf(t, testMsg, i, "expected %d but got %d", testMsg, i)
|
||||||
|
feedback = append(feedback, 1)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
func(i int) bool {
|
||||||
|
assert.Equalf(t, testMsg, i, "expected %d but got %d", testMsg, i)
|
||||||
|
feedback = append(feedback, 2)
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
func(i int) bool {
|
||||||
|
assert.Equalf(t, testMsg, i, "expected %d but got %d", testMsg, i)
|
||||||
|
feedback = append(feedback, 3)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSubscribers := sequentialDelivery(testMsg, subscribers)
|
||||||
|
|
||||||
|
assert.Len(t, nextSubscribers, len(subscribers)-1, "expected to have one less subscriber")
|
||||||
|
assert.Len(t, feedback, 3, "one or more subscriber was not called")
|
||||||
|
|
||||||
|
finalSubscribers := sequentialDelivery(testMsg, nextSubscribers)
|
||||||
|
|
||||||
|
assert.Len(t, finalSubscribers, len(nextSubscribers), "expected to have the same subscribers")
|
||||||
|
assert.Len(t, feedback, 5, "one or more subscriber was not called")
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module gitlab.com/naterciom/gubgub
|
module gitlab.com/naterciom/gubgub
|
||||||
|
|
||||||
go 1.22.5
|
go 1.23
|
||||||
|
|
||||||
require github.com/stretchr/testify v1.9.0
|
require github.com/stretchr/testify v1.9.0
|
||||||
|
|
||||||
|
|||||||
BIN
gubgub.test
Executable file
BIN
gubgub.test
Executable file
Binary file not shown.
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkSyncTopic_Publish(b *testing.B) {
|
func BenchmarkSyncTopic_Publish(b *testing.B) {
|
||||||
for _, tc := range benchTestCase {
|
for _, tc := range publishCases {
|
||||||
b.Run(tc.Name, func(b *testing.B) {
|
b.Run(tc.Name, func(b *testing.B) {
|
||||||
topic := NewSyncTopic[int]()
|
topic := NewSyncTopic[int]()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user