pax_global_header00006660000000000000000000000064152067422750014524gustar00rootroot0000000000000052 comment=f2ca57f369de6e6fe21933ede1f04383aa8e053b golang-github-ysmood-goob-0.4.0/000077500000000000000000000000001520674227500165105ustar00rootroot00000000000000golang-github-ysmood-goob-0.4.0/.github/000077500000000000000000000000001520674227500200505ustar00rootroot00000000000000golang-github-ysmood-goob-0.4.0/.github/workflows/000077500000000000000000000000001520674227500221055ustar00rootroot00000000000000golang-github-ysmood-goob-0.4.0/.github/workflows/test.yml000066400000000000000000000007151520674227500236120ustar00rootroot00000000000000name: Test on: push: pull_request: env: GODEBUG: tracebackancestors=1000 jobs: linux: runs-on: ubuntu-latest steps: - uses: actions/setup-go@v2 with: go-version: 1.17 - uses: actions/checkout@v2 - name: lint run: go run github.com/ysmood/golangci-lint@latest - name: test run: | go test -race -coverprofile=coverage.out ./... go run github.com/ysmood/got/cmd/check-cov@latest golang-github-ysmood-goob-0.4.0/.gitignore000066400000000000000000000000051520674227500204730ustar00rootroot00000000000000*.outgolang-github-ysmood-goob-0.4.0/.golangci.yml000066400000000000000000000003051520674227500210720ustar00rootroot00000000000000 run: skip-dirs-use-default: false linters: enable: - gofmt - revive - gocyclo - misspell - bodyclose gocyclo: min-complexity: 15 issues: exclude-use-default: false golang-github-ysmood-goob-0.4.0/LICENSE000066400000000000000000000020511520674227500175130ustar00rootroot00000000000000The MIT License Copyright 2019 Yad Smood Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.golang-github-ysmood-goob-0.4.0/examples_test.go000066400000000000000000000007201520674227500217130ustar00rootroot00000000000000package goob_test import ( "context" "fmt" "time" "github.com/ysmood/goob" ) func Example_basic() { ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() // create an observable instance ob := goob.New(ctx) events := ob.Subscribe(context.TODO()) // publish events without blocking ob.Publish(1) ob.Publish(2) ob.Publish(3) // consume events for e := range events { fmt.Print(e) } // Output: 123 } golang-github-ysmood-goob-0.4.0/go.mod000066400000000000000000000001211520674227500176100ustar00rootroot00000000000000module github.com/ysmood/goob go 1.15 require github.com/ysmood/gotrace v0.6.0 golang-github-ysmood-goob-0.4.0/go.sum000066400000000000000000000002511520674227500176410ustar00rootroot00000000000000github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= golang-github-ysmood-goob-0.4.0/goob.go000066400000000000000000000021101520674227500177570ustar00rootroot00000000000000package goob import ( "context" "sync" ) // Observable hub type Observable struct { ctx context.Context lock *sync.Mutex subscribers map[Events]func(Event) } // New observable instance func New(ctx context.Context) *Observable { ob := &Observable{ ctx: ctx, lock: &sync.Mutex{}, subscribers: map[Events]func(Event){}, } return ob } // Publish message to the queue func (ob *Observable) Publish(e Event) { ob.lock.Lock() defer ob.lock.Unlock() for _, write := range ob.subscribers { write(e) } } // Subscribe message func (ob *Observable) Subscribe(ctx context.Context) Events { ob.lock.Lock() defer ob.lock.Unlock() ctx, cancel := context.WithCancel(ctx) write, events := NewPipe(ctx) ob.subscribers[events] = write go func() { select { case <-ctx.Done(): case <-ob.ctx.Done(): } ob.lock.Lock() defer ob.lock.Unlock() delete(ob.subscribers, events) cancel() }() return events } // Len of the subscribers func (ob *Observable) Len() int { ob.lock.Lock() defer ob.lock.Unlock() return len(ob.subscribers) } golang-github-ysmood-goob-0.4.0/goob_test.go000066400000000000000000000074701520674227500210340ustar00rootroot00000000000000package goob_test import ( "context" "math/rand" "reflect" "runtime" "sync" "sync/atomic" "testing" "time" "github.com/ysmood/goob" "github.com/ysmood/gotrace" ) func checkLeak(t *testing.T) { gotrace.CheckLeak(t, 0) } type null struct{} func eq(t *testing.T, expected, actual interface{}) { if !reflect.DeepEqual(expected, actual) { t.Error(expected, "not equal", actual) } } func TestNew(t *testing.T) { checkLeak(t) ctx, cancel := context.WithCancel(context.Background()) defer t.Cleanup(cancel) ob := goob.New(ctx) s := ob.Subscribe(ctx) size := 1000 expected := []int{} go func() { for i := range make([]null, size) { expected = append(expected, i) ob.Publish(i) } }() result := []int{} for msg := range s { result = append(result, msg.(int)) if len(result) == size { break } } eq(t, expected, result) } func TestCancel(t *testing.T) { checkLeak(t) ob := goob.New(context.Background()) ctx, cancel := context.WithCancel(context.Background()) ob.Subscribe(ctx) cancel() time.Sleep(10 * time.Millisecond) eq(t, ob.Len(), 0) } func TestClosed(t *testing.T) { checkLeak(t) ctx, cancel := context.WithCancel(context.Background()) ob := goob.New(ctx) ob.Subscribe(ctx) cancel() s := ob.Subscribe(context.Background()) _, ok := <-s ob.Publish(1) eq(t, ok, false) eq(t, ob.Len(), 0) } func TestMultipleConsumers(t *testing.T) { checkLeak(t) ctx, cancel := context.WithCancel(context.Background()) defer t.Cleanup(cancel) ob := goob.New(ctx) s1 := ob.Subscribe(ctx) s2 := ob.Subscribe(ctx) s3 := ob.Subscribe(ctx) size := 1000 expected := []int{} go func() { for i := range make([]null, size) { expected = append(expected, i) time.Sleep(time.Duration(rand.Intn(100)) * time.Nanosecond) ob.Publish(i) } }() wg := sync.WaitGroup{} wg.Add(2) r1 := []int{} go func() { for e := range s1 { r1 = append(r1, e.(int)) if len(r1) == size { wg.Done() } } }() r2 := []int{} go func() { for e := range s2 { r2 = append(r2, e.(int)) if len(r2) == size { wg.Done() } } }() go func() { <-s3 // simulate slow consumer }() wg.Wait() eq(t, expected, r1) eq(t, expected, r2) } func TestSlowConsumer(t *testing.T) { checkLeak(t) ctx, cancel := context.WithCancel(context.Background()) defer t.Cleanup(cancel) ob := goob.New(ctx) s := ob.Subscribe(ctx) ob.Publish(1) time.Sleep(20 * time.Millisecond) <-s } func TestMonkey(t *testing.T) { checkLeak(t) count := int32(0) roundSize := 20 size := 1000 wg := sync.WaitGroup{} wg.Add(roundSize) run := func() { defer wg.Done() ctx, cancel := context.WithCancel(context.Background()) defer cancel() ob := goob.New(ctx) s := ob.Subscribe(ctx) go func() { for range make([]null, size) { time.Sleep(time.Duration(rand.Intn(100)) * time.Nanosecond) ob.Publish(nil) } }() wait := make(chan null) go func() { for i := range make([]null, size) { time.Sleep(time.Duration(rand.Intn(100)) * time.Nanosecond) <-s atomic.AddInt32(&count, 1) if i == size-1 { wait <- null{} } } }() <-wait } for range make([]null, roundSize) { go run() } wg.Wait() eq(t, roundSize*size, int(count)) } func BenchmarkPublish(b *testing.B) { ctx, cancel := context.WithCancel(context.Background()) defer b.Cleanup(cancel) ob := goob.New(ctx) s := ob.Subscribe(ctx) for i := 0; i < runtime.NumCPU(); i++ { go func() { for range s { } }() } b.ResetTimer() b.RunParallel(func(p *testing.PB) { for p.Next() { ob.Publish(nil) } }) } func BenchmarkConsume(b *testing.B) { ctx, cancel := context.WithCancel(context.Background()) defer b.Cleanup(cancel) ob := goob.New(ctx) s := ob.Subscribe(ctx) for i := 0; i < b.N; i++ { ob.Publish(nil) } b.ResetTimer() for i := 0; i < b.N; i++ { <-s } } golang-github-ysmood-goob-0.4.0/pipe.go000066400000000000000000000017571520674227500200060ustar00rootroot00000000000000package goob import ( "context" "sync" ) // Event interface type Event interface{} // Events channel type Events <-chan Event // NewPipe instance. // Pipe the Event via Write to Events. Events uses an internal buffer so it won't block Write. func NewPipe(ctx context.Context) (Write func(Event), Events <-chan Event) { events := make(chan Event) lock := sync.Mutex{} buf := []Event{} // using slice is faster than linked-list in general cases wait := make(chan struct{}, 1) write := func(e Event) { lock.Lock() defer lock.Unlock() buf = append(buf, e) if len(wait) == 0 { select { case <-ctx.Done(): return case wait <- struct{}{}: } } } go func() { defer close(events) for { lock.Lock() section := buf buf = []Event{} lock.Unlock() for _, e := range section { select { case <-ctx.Done(): return case events <- e: } } select { case <-ctx.Done(): return case <-wait: } } }() return write, events } golang-github-ysmood-goob-0.4.0/pipe_test.go000066400000000000000000000030301520674227500210270ustar00rootroot00000000000000package goob_test import ( "context" "sync" "testing" "time" "github.com/ysmood/goob" ) func TestPipeOrder(t *testing.T) { checkLeak(t) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) write, events := goob.NewPipe(ctx) write(1) write(2) write(3) if 1 != <-events { t.Fatal() } if 2 != <-events { t.Fatal() } if 3 != <-events { t.Fatal() } } func TestPipe(t *testing.T) { checkLeak(t) const pipeCount = 10 const msgCount = 10 round := func() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() write, events := goob.NewPipe(ctx) wg := sync.WaitGroup{} wg.Add(msgCount) for i := 0; i < msgCount*2; i++ { if i%2 == 0 { go write(i) } else { go func() { <-events wg.Done() }() } } wg.Wait() } wg := sync.WaitGroup{} wg.Add(pipeCount) for i := 0; i < pipeCount; i++ { go func() { round() wg.Done() }() } wg.Wait() } func TestPipeCancel(t *testing.T) { checkLeak(t) const count = 1000 for i := 0; i < count; i++ { ctx, cancel := context.WithCancel(context.Background()) write, _ := goob.NewPipe(ctx) go write(1) go cancel() } } func TestPipeMonkey(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) write, events := goob.NewPipe(ctx) round := 30 count := 10000 for i := 0; i < round; i++ { go func() { for i := 0; i < count; i++ { write(i) } }() } for i := 0; i < count*round; i++ { time.Sleep(100 * time.Nanosecond) <-events } } golang-github-ysmood-goob-0.4.0/readme.md000066400000000000000000000012231520674227500202650ustar00rootroot00000000000000# Overview A lightweight observable lib. Go channel doesn't support unlimited buffer size, it's a pain to decide what size to use, this lib will handle it dynamically. - unlimited buffer size - one publisher to multiple subscribers - thread-safe - subscribers never block each other - stable event order ## Examples See [examples_test.go](examples_test.go). ## Benchmark ```txt goos: darwin goarch: amd64 pkg: github.com/ysmood/goob cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz BenchmarkPublish-12 7493547 143.9 ns/op 86 B/op 0 allocs/op BenchmarkConsume-12 4258910 275.5 ns/op 0 B/op 0 allocs/op ```