├── errgroup ├── go.sum ├── go.mod ├── Makefile ├── LICENSE.google ├── README.md ├── doc.go ├── panic.go ├── ctxgroup.go ├── panic_test.go ├── errgroup.go ├── ctxgroup_test.go └── errgroup_test.go ├── eventstream ├── go.sum ├── go.mod ├── test │ ├── go.mod │ ├── Makefile │ ├── go.sum │ └── eventstream_test.go ├── node.go ├── iterator.go ├── Makefile ├── eventstream.go ├── interface.go └── README.md ├── examples └── chatterbox │ ├── go.mod │ ├── Makefile │ ├── chatserver │ ├── util.go │ ├── model.go │ └── server.go │ ├── chatterbox.proto │ ├── README.md │ ├── chatclient │ ├── util.go │ ├── client.go │ └── monitor.go │ ├── model.go │ ├── cmd │ └── chatterbox │ │ └── chatterbox.go │ ├── chatterbox_grpc.pb.go │ └── chatterbox.pb.go ├── README.md ├── LICENSE └── .circleci └── config.yml /errgroup/go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eventstream/go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /errgroup/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fullstorydev/go/errgroup 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /eventstream/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fullstorydev/go/eventstream 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /examples/chatterbox/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fullstorydev/go/examples/chatterbox 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fullstorydev/go/eventstream v0.0.0-20211031163310-f3206704c9cb 7 | golang.org/x/net v0.33.0 // indirect 8 | google.golang.org/grpc v1.56.3 9 | google.golang.org/protobuf v1.33.0 10 | ) 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go 2 | General purpose utility libraries for Go. Low dependency. 3 | - [eventstream](eventstream) is a fast, efficient way to publish and subscribe to events within a Go process. 4 | - [errgroup](errgroup) is a safer alternative to the official [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) package. 5 | -------------------------------------------------------------------------------- /eventstream/test/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fullstorydev/go/eventstream/test 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/fullstorydev/go/eventstream v0.0.0 7 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 8 | gotest.tools/v3 v3.0.3 9 | ) 10 | 11 | require ( 12 | github.com/google/go-cmp v0.4.0 // indirect 13 | github.com/pkg/errors v0.8.1 // indirect 14 | ) 15 | 16 | replace github.com/fullstorydev/go/eventstream => ./.. 17 | -------------------------------------------------------------------------------- /examples/chatterbox/Makefile: -------------------------------------------------------------------------------- 1 | all: $(GOPATH)/bin/chatterbox 2 | 3 | %.pb.go %_grpc.pb.go: %.proto 4 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26 5 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1 6 | protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative $< 7 | 8 | $(GOPATH)/bin/chatterbox: chatterbox.pb.go chatterbox_grpc.pb.go $(shell find . -name "*.go" -type f) 9 | go install ./cmd/chatterbox 10 | -------------------------------------------------------------------------------- /examples/chatterbox/chatserver/util.go: -------------------------------------------------------------------------------- 1 | package chatserver 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "google.golang.org/grpc/codes" 8 | "google.golang.org/grpc/status" 9 | ) 10 | 11 | // filterServerError cleans up error logging by filtering out errors related to (probably user initiated) cancel. 12 | func filterServerError(err error) error { 13 | if err == io.EOF || err == context.Canceled { 14 | return nil 15 | } 16 | if code := status.Code(err); code == codes.Canceled { 17 | return nil 18 | } 19 | return err 20 | } 21 | -------------------------------------------------------------------------------- /examples/chatterbox/chatterbox.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/fullstorydev/go/examples/chatterbox"; 4 | 5 | package chatterbox; 6 | 7 | import "google/protobuf/empty.proto"; 8 | 9 | service ChatterBox { 10 | // Chat joins the chat room and sends chat messages. 11 | rpc Chat(stream Send) returns (stream Event) {} 12 | 13 | 14 | // Monitor passively monitors the room. 15 | rpc Monitor(google.protobuf.Empty) returns (stream Event) {} 16 | } 17 | 18 | message Send { 19 | string text = 1; 20 | } 21 | 22 | message Event { 23 | string who = 1; 24 | What what = 2; 25 | string text = 3; 26 | } 27 | 28 | enum What { 29 | INITIALIZED = 0; // signals that the client is fully initialized 30 | CHAT = 1; 31 | JOIN = 2; 32 | LEAVE = 3; 33 | } 34 | -------------------------------------------------------------------------------- /eventstream/node.go: -------------------------------------------------------------------------------- 1 | package eventstream 2 | 3 | type node[T any] struct { 4 | value T // the value of the event (zero until ready) 5 | next *node[T] // the next node in the stream (nil until ready) 6 | ready chan struct{} // a channel whose closure marks readiness 7 | } 8 | 9 | func (n *node[T]) Ready() <-chan struct{} { 10 | return n.ready 11 | } 12 | 13 | func (n *node[T]) Next() (T, Promise[T]) { 14 | <-n.ready 15 | if n.next == nil { 16 | // force nil interface 17 | return n.value, nil 18 | } 19 | return n.value, n.next 20 | } 21 | 22 | func (n *node[T]) Iterator() Iterator[T] { 23 | return &iterator[T]{p: n} 24 | } 25 | 26 | func (n *node[T]) makeReady(v T, next *node[T]) { 27 | n.value = v 28 | n.next = next 29 | close(n.ready) 30 | } 31 | 32 | var _ Promise[any] = (*node[any])(nil) 33 | -------------------------------------------------------------------------------- /examples/chatterbox/README.md: -------------------------------------------------------------------------------- 1 | # chatterbox 2 | 3 | A simple chat server that illustrates using [EventStream](https://github.com/fullstorydev/go/tree/master/eventstream) 4 | with gRPC. 5 | 6 | ## Install 7 | 8 | ### Remote 9 | 10 | ```bash 11 | go install github.com/fullstorydev/go/examples/chatterbox/...@latest 12 | ``` 13 | 14 | ### Local 15 | 16 | ```bash 17 | git clone https://github.com/fullstorydev/go.git 18 | cd go/examples/chatterbox 19 | go install ./cmd/chatterbox 20 | ``` 21 | 22 | ## Run 23 | 24 | ```bash 25 | chatterbox server # run the server 26 | chatterbox monitor # run a monitor (in a different terminal) 27 | chatterbox client # run a client (in a different terminal) 28 | chatterbox client # run a client (in a different terminal) 29 | ``` 30 | 31 | You can run only one server, but as many clients or monitors as you want in different terminals. A client participates 32 | in chat, but a monitor just passively listens. 33 | -------------------------------------------------------------------------------- /eventstream/iterator.go: -------------------------------------------------------------------------------- 1 | package eventstream 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | type iterator[T any] struct { 9 | p Promise[T] 10 | } 11 | 12 | func (it *iterator[T]) Next(ctx context.Context) (T, error) { 13 | var zero T 14 | if it.p == nil { 15 | return zero, ErrDone 16 | } 17 | if err := ctx.Err(); err != nil { 18 | return zero, err 19 | } 20 | 21 | select { 22 | case <-it.p.Ready(): 23 | v, p := it.p.Next() 24 | it.p = p 25 | if p == nil { 26 | return zero, ErrDone 27 | } 28 | return v, nil 29 | case <-ctx.Done(): 30 | return zero, ctx.Err() 31 | } 32 | } 33 | 34 | func (it *iterator[T]) Consume(ctx context.Context, callback func(context.Context, T) error) error { 35 | for { 36 | if val, err := it.Next(ctx); err != nil { 37 | return filterErrDone(err) 38 | } else if err = callback(ctx, val); err != nil { 39 | return filterErrDone(err) 40 | } 41 | } 42 | } 43 | 44 | func filterErrDone(err error) error { 45 | if errors.Is(err, ErrDone) { 46 | return nil 47 | } 48 | return err 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Engineering at Fullstory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eventstream/test/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: ci 2 | ci: deps checkgofmt vet staticcheck ineffassign predeclared golint errcheck test 3 | 4 | .PHONY: deps 5 | deps: 6 | go get -d -v -t ./... 7 | go mod tidy 8 | 9 | .PHONY: updatedeps 10 | updatedeps: 11 | go get -d -v -t -u -f ./... 12 | go mod tidy 13 | 14 | .PHONY: checkgofmt 15 | checkgofmt: 16 | gofmt -s -l . 17 | @if [ -n "$$(gofmt -s -l .)" ]; then \ 18 | exit 1; \ 19 | fi 20 | 21 | .PHONY: vet 22 | vet: 23 | go vet 24 | 25 | .PHONY: staticcheck 26 | staticcheck: 27 | @go install honnef.co/go/tools/cmd/staticcheck@v0.5.1 28 | staticcheck ./... 29 | 30 | .PHONY: ineffassign 31 | ineffassign: 32 | @go install github.com/gordonklaus/ineffassign@7953dde2c7bf 33 | ineffassign . 34 | 35 | .PHONY: predeclared 36 | predeclared: 37 | @go install github.com/nishanths/predeclared@245576f9a85c96ea16c750df3887f1d827f01e9c 38 | predeclared ./... 39 | 40 | .PHONY: golint 41 | golint: 42 | @go install golang.org/x/lint/golint@v0.0.0-20210508222113-6edffad5e616 43 | golint -min_confidence 0.9 -set_exit_status ./... 44 | 45 | .PHONY: errcheck 46 | errcheck: 47 | @go install github.com/kisielk/errcheck@v1.2.0 48 | errcheck ./... 49 | 50 | .PHONY: test 51 | test: 52 | go test -race ./... 53 | -------------------------------------------------------------------------------- /errgroup/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: ci 2 | ci: deps checkgofmt vet staticcheck ineffassign predeclared golint errcheck test 3 | 4 | .PHONY: deps 5 | deps: 6 | go get -d -v -t ./... 7 | go mod tidy 8 | 9 | .PHONY: updatedeps 10 | updatedeps: 11 | go get -d -v -t -u -f ./... 12 | go mod tidy 13 | 14 | .PHONY: checkgofmt 15 | checkgofmt: 16 | gofmt -s -l . 17 | @if [ -n "$$(gofmt -s -l .)" ]; then \ 18 | exit 1; \ 19 | fi 20 | 21 | .PHONY: vet 22 | vet: 23 | go vet 24 | 25 | .PHONY: staticcheck 26 | staticcheck: 27 | @go install honnef.co/go/tools/cmd/staticcheck@v0.5.1 28 | staticcheck ./... 29 | 30 | .PHONY: ineffassign 31 | ineffassign: 32 | @go install github.com/gordonklaus/ineffassign@7953dde2c7bf 33 | ineffassign . 34 | 35 | .PHONY: predeclared 36 | predeclared: 37 | @go install github.com/nishanths/predeclared@245576f9a85c96ea16c750df3887f1d827f01e9c 38 | predeclared ./... 39 | 40 | .PHONY: golint 41 | golint: 42 | @go install golang.org/x/lint/golint@v0.0.0-20210508222113-6edffad5e616 43 | golint -min_confidence 0.9 -set_exit_status ./... 44 | 45 | .PHONY: errcheck 46 | errcheck: 47 | @go install github.com/kisielk/errcheck@v1.2.0 48 | errcheck ./... 49 | 50 | .PHONY: test 51 | test: 52 | # The race detector requires CGO: https://github.com/golang/go/issues/6508 53 | CGO_ENABLED=1 go test -race ./... 54 | -------------------------------------------------------------------------------- /eventstream/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: ci 2 | ci: deps checkgofmt vet staticcheck ineffassign predeclared golint errcheck test 3 | 4 | .PHONY: deps 5 | deps: 6 | go get -d -v -t ./... 7 | go mod tidy 8 | 9 | .PHONY: updatedeps 10 | updatedeps: 11 | go get -d -v -t -u -f ./... 12 | go mod tidy 13 | 14 | .PHONY: checkgofmt 15 | checkgofmt: 16 | gofmt -s -l . 17 | @if [ -n "$$(gofmt -s -l .)" ]; then \ 18 | exit 1; \ 19 | fi 20 | 21 | .PHONY: vet 22 | vet: 23 | go vet 24 | 25 | .PHONY: staticcheck 26 | staticcheck: 27 | @go install honnef.co/go/tools/cmd/staticcheck@v0.5.1 28 | staticcheck ./... 29 | 30 | .PHONY: ineffassign 31 | ineffassign: 32 | @go install github.com/gordonklaus/ineffassign@7953dde2c7bf 33 | ineffassign . 34 | 35 | .PHONY: predeclared 36 | predeclared: 37 | @go install github.com/nishanths/predeclared@245576f9a85c96ea16c750df3887f1d827f01e9c 38 | predeclared ./... 39 | 40 | .PHONY: golint 41 | golint: 42 | @go install golang.org/x/lint/golint@v0.0.0-20210508222113-6edffad5e616 43 | golint -min_confidence 0.9 -set_exit_status ./... 44 | 45 | .PHONY: errcheck 46 | errcheck: 47 | @go install github.com/kisielk/errcheck@v1.2.0 48 | errcheck ./... 49 | 50 | .PHONY: test 51 | test: 52 | # The race detector requires CGO: https://github.com/golang/go/issues/6508 53 | CGO_ENABLED=1 go test -race ./... 54 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | shared_configs: 2 | simple_job_steps: &simple_job_steps 3 | - checkout 4 | - run: 5 | name: Run tests 6 | command: | 7 | make -C errgroup test 8 | make -C eventstream test 9 | make -C eventstream/test test 10 | 11 | 12 | # Use the latest 2.1 version of CircleCI pipeline process engine. See: https://circleci.com/docs/2.0/configuration-reference 13 | version: 2.1 14 | jobs: 15 | build-1-20: 16 | working_directory: ~/repo 17 | docker: 18 | - image: cimg/go:1.20 19 | steps: *simple_job_steps 20 | 21 | build-1-21: 22 | working_directory: ~/repo 23 | docker: 24 | - image: cimg/go:1.21 25 | steps: *simple_job_steps 26 | 27 | build-1-22: 28 | working_directory: ~/repo 29 | docker: 30 | - image: cimg/go:1.22 31 | steps: *simple_job_steps 32 | 33 | build-1-23: 34 | working_directory: ~/repo 35 | docker: 36 | - image: cimg/go:1.23 37 | steps: 38 | - checkout 39 | - run: 40 | name: Run tests and linters 41 | command: | 42 | make -C errgroup ci 43 | make -C eventstream ci 44 | make -C eventstream/test ci 45 | 46 | workflows: 47 | pr-build-test: 48 | jobs: 49 | - build-1-20 50 | - build-1-21 51 | - build-1-22 52 | - build-1-23 53 | -------------------------------------------------------------------------------- /examples/chatterbox/chatclient/util.go: -------------------------------------------------------------------------------- 1 | package chatclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/fullstorydev/go/examples/chatterbox" 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/codes" 11 | "google.golang.org/grpc/status" 12 | ) 13 | 14 | // filterClientError cleans up error logging by filtering out errors related to (probably user initiated) cancel. 15 | func filterClientError(err error) error { 16 | if err == io.EOF || err == context.Canceled { 17 | return nil 18 | } 19 | if code := status.Code(err); code == codes.Canceled { 20 | return nil 21 | } 22 | return err 23 | } 24 | 25 | // commonClientStream intersects ChatterBox_ChatClient and ChatterBox_MonitorClient 26 | type commonClientStream interface { 27 | grpc.ClientStream 28 | Recv() (*chatterbox.Event, error) 29 | } 30 | 31 | // fetchInitialState ensures we read a complete initial model from the server 32 | func fetchInitialState(ctx context.Context, stream commonClientStream) (chatterbox.MembersModel, error) { 33 | // Wait for the initial state to come back. 34 | members := chatterbox.MembersModel{} 35 | for { 36 | msg, err := stream.Recv() 37 | if err != nil { 38 | return nil, fmt.Errorf("stream.Recv: %w", err) 39 | } 40 | switch msg.What { 41 | case chatterbox.What_INITIALIZED: 42 | return members, nil 43 | case chatterbox.What_JOIN: 44 | members.Add(msg.Who) 45 | default: 46 | return nil, fmt.Errorf("unexpected type: %s", msg.What) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /errgroup/LICENSE.google: -------------------------------------------------------------------------------- 1 | Portions Copyright 2009 The Go Authors. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | * Redistributions of source code must retain the above copyright 6 | notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above 8 | copyright notice, this list of conditions and the following disclaimer 9 | in the documentation and/or other materials provided with the 10 | distribution. 11 | * Neither the name of Google LLC nor the names of its 12 | contributors may be used to endorse or promote products derived from 13 | this software without specific prior written permission. 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /eventstream/test/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 2 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 4 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 5 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 6 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 7 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 8 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 9 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 10 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 11 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 12 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 13 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 14 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 15 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 16 | gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= 17 | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 18 | -------------------------------------------------------------------------------- /examples/chatterbox/chatserver/model.go: -------------------------------------------------------------------------------- 1 | package chatserver 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | 7 | "github.com/fullstorydev/go/eventstream" 8 | "github.com/fullstorydev/go/examples/chatterbox" 9 | ) 10 | 11 | // ServerMembers is a server-side Log Replicated Model tracking changes to MembersModel over time. 12 | type ServerMembers struct { 13 | mu sync.RWMutex 14 | members chatterbox.MembersModel 15 | es eventstream.EventStream 16 | } 17 | 18 | func NewMembersList() *ServerMembers { 19 | return &ServerMembers{ 20 | members: chatterbox.MembersModel{}, 21 | es: eventstream.New(), 22 | } 23 | } 24 | 25 | func (m *ServerMembers) ReadAndSubscribe() ([]string, eventstream.Promise) { 26 | m.mu.RLock() 27 | defer m.mu.RUnlock() 28 | ret := make([]string, 0, len(m.members)) 29 | for k := range m.members { 30 | ret = append(ret, k) 31 | } 32 | sort.Strings(ret) 33 | return ret, m.es.Subscribe() 34 | } 35 | 36 | func (m *ServerMembers) Join(name string) { 37 | m.mu.Lock() 38 | defer m.mu.Unlock() 39 | 40 | // apply update, publish event 41 | m.members.Add(name) 42 | m.es.Publish(&chatterbox.Event{ 43 | Who: name, 44 | What: chatterbox.What_JOIN, 45 | }) 46 | } 47 | 48 | func (m *ServerMembers) Leave(name string) { 49 | m.mu.Lock() 50 | defer m.mu.Unlock() 51 | 52 | // apply update, publish event 53 | m.members.Remove(name) 54 | m.es.Publish(&chatterbox.Event{ 55 | Who: name, 56 | What: chatterbox.What_LEAVE, 57 | }) 58 | } 59 | 60 | func (m *ServerMembers) Chat(name string, text string) { 61 | m.es.Publish(&chatterbox.Event{ 62 | Who: name, 63 | What: chatterbox.What_CHAT, 64 | Text: text, 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /eventstream/eventstream.go: -------------------------------------------------------------------------------- 1 | package eventstream 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | const defaultBufferSize = 1024 8 | 9 | type eventStream[T any] struct { 10 | tail *node[T] // the current tail, as used only by the publisher 11 | subscribeTail atomic.Value // the current tail, as published to subscribers 12 | 13 | buffer []node[T] // a buffer of nodes to use 14 | bufferPos int // the next available node within buffer 15 | } 16 | 17 | var _ EventStream[any] = (*eventStream[any])(nil) 18 | 19 | func (e *eventStream[T]) Publish(v T) { 20 | if e.buffer == nil { 21 | panic("closed") 22 | } 23 | pub := e.tail 24 | nextTail := e.initNextTail() 25 | pub.makeReady(v, nextTail) 26 | } 27 | 28 | func (e *eventStream[T]) Close() { 29 | var zero T 30 | e.buffer = nil 31 | e.bufferPos = 0 32 | e.tail.makeReady(zero, nil) 33 | } 34 | 35 | func (e *eventStream[T]) Subscribe() Promise[T] { 36 | return e.subscribeTail.Load().(*node[T]) 37 | } 38 | 39 | func (e *eventStream[T]) initNextTail() *node[T] { 40 | if e.bufferPos >= len(e.buffer) { 41 | e.buffer = make([]node[T], len(e.buffer)) 42 | e.bufferPos = 0 43 | } 44 | 45 | newTail := &e.buffer[e.bufferPos] 46 | e.bufferPos++ 47 | newTail.ready = make(chan struct{}) 48 | e.tail = newTail 49 | e.subscribeTail.Store(newTail) 50 | return newTail 51 | } 52 | 53 | // New creates an EventStream with the default buffer size. 54 | func New[T any]() EventStream[T] { 55 | return NewWithBuffer[T](defaultBufferSize) 56 | } 57 | 58 | // NewWithBuffer creates an EventStream with the given buffer size. 59 | func NewWithBuffer[T any](bufferSize int) EventStream[T] { 60 | if bufferSize < 1 { 61 | panic("invalid buffer size") 62 | } 63 | ret := &eventStream[T]{ 64 | buffer: make([]node[T], bufferSize), 65 | bufferPos: 0, 66 | } 67 | ret.initNextTail() 68 | return ret 69 | } 70 | -------------------------------------------------------------------------------- /examples/chatterbox/model.go: -------------------------------------------------------------------------------- 1 | package chatterbox 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | // MembersModel is a basic model object representing the current users in the room. 9 | // Used by both client and server. For this app, we've made a couple of design choices: 10 | // 11 | // 1) Copy-on-read instead of copy-on-write. 12 | // 2) Simple direct mutators (Add/Remove). 13 | // 14 | // This implementation requires external synchronization. Contrast with MembersModelAlt. 15 | type MembersModel map[string]struct{} 16 | 17 | // Add adds a user to the set. 18 | func (mm MembersModel) Add(name string) { 19 | mm[name] = struct{}{} 20 | } 21 | 22 | // Remove removes user to the set. 23 | func (mm MembersModel) Remove(name string) { 24 | delete(mm, name) 25 | } 26 | 27 | // Strings returns a copy of the current list of members. 28 | func (mm MembersModel) Strings() []string { 29 | var ret []string 30 | for k := range mm { 31 | ret = append(ret, k) 32 | } 33 | sort.Strings(ret) 34 | return ret 35 | } 36 | 37 | func (mm MembersModel) String() string { 38 | return strings.Join(mm.Strings(), ", ") 39 | } 40 | 41 | // MembersModelAlt is alternative model object representing the current users in the room, but 42 | // with the opposite set of design choices: 43 | // 44 | // 1) Copy-on-write instead of copy-on-read. 45 | // 2) Applies mutation events rather than direct mutators. 46 | // 47 | // This implementation does not require external synchronization. Contrast with MembersModel. 48 | type MembersModelAlt []string 49 | 50 | // ApplyMutation returns a copy of the current model with the given mutation applied. 51 | func (mma MembersModelAlt) ApplyMutation(evt *Event) MembersModelAlt { 52 | switch evt.What { 53 | case What_JOIN: 54 | // Create a new version with a new member. 55 | ret := append(MembersModelAlt{evt.Who}, mma...) 56 | sort.Strings(ret) 57 | return ret 58 | case What_LEAVE: 59 | // Create a new version with the member filtered out. 60 | ret := make(MembersModelAlt, 0, len(mma)) 61 | for _, v := range mma { 62 | if v != evt.Who { 63 | ret = append(ret, v) 64 | } 65 | } 66 | return ret 67 | default: 68 | return mma // other events are no-ops 69 | } 70 | } 71 | 72 | // Strings returns a copy of the current list of members. 73 | func (mm MembersModelAlt) Strings() []string { 74 | return mm 75 | } 76 | 77 | func (mm MembersModelAlt) String() string { 78 | return strings.Join(mm, ", ") 79 | } 80 | -------------------------------------------------------------------------------- /errgroup/README.md: -------------------------------------------------------------------------------- 1 | # errgroup 2 | 3 | `errgroup` is a safer alternative to the official [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) package. 4 | 5 | ## errgroup.Group 6 | 7 | `Group` is an API-compatible, drop-in replacement for [golang.org/x/sync/errgroup.Group](https://pkg.go.dev/golang.org/x/sync/errgroup#Group), 8 | with the key difference that panics thrown from subtasks will not crash the Go process. 9 | Any panics are caught and wrapped in a `PanicError`. If this is the first error returned from 10 | a subtask, this error will be the error returned from `Wait()`. 11 | 12 | ### Using 13 | 14 | You can start using this today simply by changing your import statements. 15 | 16 | ```diff 17 | -import "golang.org/x/sync/errgroup" 18 | +import "github.com/fullstorydev/go/errgroup" 19 | ``` 20 | 21 | ## errgroup.ContextGroup 22 | 23 | `ContextGroup` takes safety a step further: it is an alternative to [golang.org/x/sync/errgroup.WithContext()](https://pkg.go.dev/golang.org/x/sync/errgroup#WithContext). 24 | `ContextGroup` owns and manages the group context, and its updated API requires subtask functions to accept a `context.Context` argument to use the group context. 25 | This helps elimate a class of bugs where subtasks accidentally use a parent context rather than the correct group context. 26 | 27 | ### Features 28 | 29 | - Automatically catches panics 30 | - Forces the correct context 31 | - Manages the context lifetime 32 | - Early-exits any calls to Go() or TryGo() once the group context is cancelled 33 | - Enforces that any `Limit` immutable and set at construction time. 34 | 35 | ### Using 36 | 37 | Create a new `ContextGroup` via `errgroup.New(ctx)` or `errgroup.WithLimit(4).New(ctx)`. 38 | 39 | Old code: 40 | ```go 41 | func fetchUrls(ctx context.Context, urls []string) ([]string, error) { 42 | ret := make([]string, len(urls)) 43 | g, gCtx := errgroup.WithContext(ctx) 44 | 45 | for i, url := range urls { 46 | i, url := i, url 47 | g.Go(func() error { 48 | // remember to use the correct ctx here 49 | body, err := httpGet(gCtx, url) 50 | ret[i] = body 51 | return err 52 | }) 53 | } 54 | return ret, g.Wait() 55 | } 56 | ``` 57 | 58 | New code: 59 | ```go 60 | func fetchUrls(ctx context.Context, urls []string) ([]string, error) { 61 | ret := make([]string, len(urls)) 62 | g := errgroup.New(ctx) 63 | 64 | for i, url := range urls { 65 | i, url := i, url 66 | g.Go(func(ctx context.Context) error { 67 | body, err := httpGet(ctx, url) 68 | ret[i] = body 69 | return err 70 | }) 71 | } 72 | return ret, g.Wait() 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /examples/chatterbox/cmd/chatterbox/chatterbox.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "net" 10 | "os" 11 | 12 | "github.com/fullstorydev/go/examples/chatterbox" 13 | "github.com/fullstorydev/go/examples/chatterbox/chatclient" 14 | "github.com/fullstorydev/go/examples/chatterbox/chatserver" 15 | "google.golang.org/grpc" 16 | ) 17 | 18 | const ( 19 | addr = "127.0.0.1:9000" 20 | ) 21 | 22 | func main() { 23 | flag.Parse() 24 | 25 | ctx, cancel := context.WithCancel(context.Background()) 26 | defer cancel() 27 | 28 | var err error 29 | switch flag.Arg(0) { 30 | case "": 31 | err = fmt.Errorf(`choose one of: "server", "client", "monitor" `) 32 | case "server": 33 | err = runServer(ctx) 34 | case "client": 35 | err = runClient(ctx) 36 | case "monitor": 37 | err = runMonitor(ctx) 38 | default: 39 | err = fmt.Errorf("unknown command: %s", flag.Arg(0)) 40 | } 41 | 42 | if err != nil { 43 | log.Println(err) 44 | os.Exit(1) 45 | } 46 | } 47 | 48 | func runServer(_ context.Context) error { 49 | svr := grpc.NewServer() 50 | chatterbox.RegisterChatterBoxServer(svr, chatserver.NewServer()) 51 | 52 | lis, err := net.Listen("tcp", addr) 53 | if err != nil { 54 | return fmt.Errorf("listen: %w", err) 55 | } 56 | log.Println("Listening on ", addr) 57 | return svr.Serve(lis) 58 | } 59 | 60 | func runClient(ctx context.Context) error { 61 | log.Println("Dialing ", addr) 62 | conn, err := grpc.DialContext(ctx, addr, grpc.WithInsecure()) 63 | if err != nil { 64 | return fmt.Errorf("dial: %w", err) 65 | } 66 | defer conn.Close() 67 | 68 | // Read lines off the terminal, try to send through channel. 69 | ctx, cancel := context.WithCancel(ctx) 70 | chatInput := make(chan string) 71 | go func() { 72 | // when exiting for any reason, cancel the stream context. 73 | defer cancel() 74 | defer close(chatInput) 75 | scanner := bufio.NewScanner(os.Stdin) 76 | for scanner.Scan() { 77 | select { 78 | case <-ctx.Done(): 79 | return 80 | case chatInput <- scanner.Text(): 81 | } 82 | } 83 | 84 | if err := scanner.Err(); err != nil { 85 | log.Println(err) 86 | } 87 | }() 88 | 89 | return chatclient.RunClient(ctx, chatInput, chatterbox.NewChatterBoxClient(conn)) 90 | } 91 | 92 | func runMonitor(ctx context.Context) error { 93 | log.Println("Dialing ", addr) 94 | conn, err := grpc.DialContext(ctx, addr, grpc.WithInsecure()) 95 | if err != nil { 96 | return fmt.Errorf("dial: %w", err) 97 | } 98 | defer conn.Close() 99 | 100 | return chatclient.RunMonitor(ctx, chatterbox.NewChatterBoxClient(conn)) 101 | } 102 | -------------------------------------------------------------------------------- /errgroup/doc.go: -------------------------------------------------------------------------------- 1 | // Package errgroup [golang.org/x/sync/errgroup.Group], providing both backwards compatibility, 2 | // but also introducing new, safer APIs. 3 | package errgroup 4 | 5 | import "context" 6 | 7 | // ErrGroup defines a compatibility interface between [Group] and [golang.org/x/sync/errgroup.Group]. 8 | type ErrGroup interface { 9 | // Go calls the given function in a new goroutine, passing the group context. 10 | // 11 | // The first call to return a non-nil error cancels the group's context. 12 | // The error will be returned by Wait. 13 | Go(func() error) 14 | // Wait blocks until all function calls from the Go method have returned, then 15 | // returns the first non-nil error (if any) from them. 16 | Wait() error 17 | // TryGo calls the given function in a new goroutine only if the number of 18 | // active goroutines in the group is currently below the configured limit. 19 | // 20 | // The return value reports whether the goroutine was started. 21 | TryGo(func() error) bool 22 | // SetLimit limits the number of active goroutines in this group to at most n. 23 | // A negative value indicates no limit. 24 | // 25 | // Any subsequent call to the Go method will block until it can add an active 26 | // goroutine without exceeding the configured limit. 27 | // 28 | // The limit must not be modified while any goroutines in the group are active. 29 | SetLimit(n int) 30 | } 31 | 32 | // ContextGroup is a variant of [golang.org/x/sync/errgroup.Group] that: 33 | // - automatically catches panics 34 | // - forces the correct context 35 | // - manages the context lifetime 36 | // - early-exits any calls to Go() or TryGo() once the context is canceled 37 | // 38 | // Unlike the golang version, ContextGroup cannot be reused after Wait() has 39 | // been called, because the context is dead and no new go funcs will be run. 40 | type ContextGroup interface { 41 | // Go calls the given function in a new goroutine, passing the group context. 42 | // 43 | // The first call to return a non-nil error cancels the group's context. 44 | // The error will be returned by Wait(). 45 | // 46 | // Go returns immediately if the group context is already cancelled. 47 | Go(func(context.Context) error) 48 | // Wait blocks until all function calls from the Go method have returned, then 49 | // returns the first non-nil error (if any) from them. 50 | Wait() error 51 | // TryGo calls the given function in a new goroutine only if the number of 52 | // active goroutines in the group is currently below the configured limit. 53 | // 54 | // The return value reports whether the goroutine was started. 55 | // TryGo returns true immediately if the group context is already cancelled. 56 | TryGo(func(context.Context) error) bool 57 | } 58 | -------------------------------------------------------------------------------- /errgroup/panic.go: -------------------------------------------------------------------------------- 1 | package errgroup 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | pcFrames = 1 << 8 12 | stackSize = 1 << 16 13 | ) 14 | 15 | var ( 16 | newline = []byte{'\n'} 17 | ) 18 | 19 | // NewPanicError creates a new PanicError with the given recovered panic value. 20 | // The caller stack frames are captured and attached, starting with the caller of NewPanicError. 21 | func NewPanicError(recovered any) *PanicError { 22 | return NewPanicErrorCallers(recovered, 2) 23 | } 24 | 25 | // NewPanicErrorCallers creates a new PanicError with the given recovered panic value. 26 | // 27 | // The argument skip is the number of stack frames to skip before recording in pc, 28 | // with 0 identifying the frame for NewPanicErrorCallers itself and 1 identifying the 29 | // caller of NewPanicErrorCallers. 30 | func NewPanicErrorCallers(recovered any, skip int) *PanicError { 31 | pcs := make([]uintptr, pcFrames) 32 | pcs = pcs[:runtime.Callers(skip+1, pcs)] 33 | 34 | // Manually prune the string stack trace based on skip (this is awkward). 35 | skipLines := 1 + 2*skip 36 | stack := make([]byte, stackSize) 37 | stack = stack[:runtime.Stack(stack, false)] 38 | var sb strings.Builder 39 | for i, line := range bytes.Split(stack, newline) { 40 | if i == 0 || i >= skipLines { 41 | sb.Write(line) 42 | sb.WriteByte('\n') 43 | } 44 | } 45 | 46 | return &PanicError{recovered: recovered, pcs: pcs, stack: sb.String()} 47 | } 48 | 49 | // PanicError represents a wrapped recovered panic value. 50 | type PanicError struct { 51 | recovered any 52 | pcs []uintptr 53 | stack string 54 | } 55 | 56 | var _ error = (*PanicError)(nil) 57 | 58 | // Recovered returns the original value. 59 | func (e *PanicError) Recovered() any { 60 | return e.recovered 61 | } 62 | 63 | // Error returns the full error, including stack trace. 64 | func (e *PanicError) Error() string { 65 | return fmt.Sprintf("panic: %v [recovered]\n\n%s", e.recovered, e.stack) 66 | } 67 | 68 | // Message returns a short description, with the string value of the recovered object. 69 | func (e *PanicError) Message() string { 70 | return fmt.Sprintf("panic: %v", e.recovered) 71 | } 72 | 73 | // Unwrap returns the recovered value if it is itself an error, 74 | // otherwise returns nil. 75 | func (e *PanicError) Unwrap() error { 76 | if wrapped, ok := e.recovered.(error); ok { 77 | return wrapped 78 | } 79 | return nil 80 | } 81 | 82 | // StackFrames returns a slice of program counters composing this error's stacktrace. 83 | func (e *PanicError) StackFrames() []uintptr { 84 | return append([]uintptr{}, e.pcs...) 85 | } 86 | 87 | // StackTrace returns the originally captured stack trace as a multiline string. 88 | func (e *PanicError) StackTrace() string { 89 | return e.stack 90 | } 91 | -------------------------------------------------------------------------------- /examples/chatterbox/chatserver/server.go: -------------------------------------------------------------------------------- 1 | package chatserver 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "sync/atomic" 7 | 8 | "github.com/fullstorydev/go/examples/chatterbox" 9 | "google.golang.org/grpc" 10 | "google.golang.org/protobuf/types/known/emptypb" 11 | ) 12 | 13 | type Server struct { 14 | chatterbox.UnimplementedChatterBoxServer 15 | 16 | model *ServerMembers 17 | lastId int64 18 | } 19 | 20 | func NewServer() *Server { 21 | return &Server{ 22 | model: NewMembersList(), 23 | lastId: 0, 24 | } 25 | } 26 | 27 | var _ chatterbox.ChatterBoxServer = (*Server)(nil) 28 | 29 | func (s *Server) Chat(server chatterbox.ChatterBox_ChatServer) error { 30 | // Make up a name for this connection. 31 | id := atomic.AddInt64(&s.lastId, 1) 32 | name := fmt.Sprintf("User %d", id) 33 | 34 | // Join the memberslist. 35 | s.model.Join(name) 36 | log.Printf("%s joined", name) 37 | defer log.Printf("%s left", name) 38 | defer s.model.Leave(name) 39 | 40 | // We do not wait on the recv loop to exit; it will exit after we return. 41 | go func() { 42 | if err := s.recvLoop(name, server); err != nil { 43 | log.Printf("%s err: %s", name, err) 44 | } 45 | }() 46 | 47 | // Run the send loop in the foreground. 48 | return s.sendLoop(server) 49 | } 50 | 51 | func (s *Server) Monitor(_ *emptypb.Empty, server chatterbox.ChatterBox_MonitorServer) error { 52 | // Don't join, just monitor. 53 | return s.sendLoop(server) 54 | } 55 | 56 | func (s *Server) recvLoop(name string, server chatterbox.ChatterBox_ChatServer) error { 57 | for { 58 | req, err := server.Recv() 59 | if err != nil { 60 | return filterServerError(err) 61 | } 62 | 63 | s.model.Chat(name, req.Text) 64 | log.Printf("%s: %s", name, req.Text) 65 | } 66 | } 67 | 68 | // commonServerStream intersects ChatterBox_ChatServer and ChatterBox_MonitorServer 69 | type commonServerStream interface { 70 | grpc.ServerStream 71 | Send(*chatterbox.Event) error 72 | } 73 | 74 | func (s *Server) sendLoop(server commonServerStream) error { 75 | members, eventPromise := s.model.ReadAndSubscribe() 76 | 77 | // Send the initial members. 78 | for _, m := range members { 79 | if err := server.Send(&chatterbox.Event{ 80 | Who: m, 81 | What: chatterbox.What_JOIN, 82 | }); err != nil { 83 | return filterServerError(err) 84 | } 85 | } 86 | 87 | // Signal ready. 88 | if err := server.Send(&chatterbox.Event{ 89 | What: chatterbox.What_INITIALIZED, 90 | }); err != nil { 91 | return filterServerError(err) 92 | } 93 | 94 | for { 95 | select { 96 | case <-server.Context().Done(): 97 | return nil 98 | case <-eventPromise.Ready(): 99 | evt, nextPromise := eventPromise.Next() 100 | if nextPromise == nil { 101 | // end of stream, should never happen 102 | return nil 103 | } 104 | eventPromise = nextPromise 105 | 106 | if err := server.Send(evt.(*chatterbox.Event)); err != nil { 107 | return filterServerError(err) 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /eventstream/interface.go: -------------------------------------------------------------------------------- 1 | // Package eventstream implements a single-producer, multiple-consumer event stream. 2 | package eventstream 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | ) 8 | 9 | // EventStream allows a single producer to publish events to multiple asynchronous consumers, who each 10 | // receive all events. Note that event values are opaque to the event stream and no copies are made: 11 | // consumers should not mutate consumed event values! 12 | type EventStream[T any] interface { 13 | // Publish adds the next value to the stream. This method can be called concurrently with subscriber reads, 14 | // but not with other publisher operations. External synchronization is required if there are multiple concurrent 15 | // publishers. 16 | Publish(T) 17 | 18 | // Close ends the stream; the same concurrency rules apply to Close() and Publish(). 19 | Close() 20 | 21 | // Subscribe returns a Promise to the next unpublished event. The returned Promise gives the caller 22 | // the events in the stream from the current position forward. 23 | Subscribe() Promise[T] 24 | } 25 | 26 | // Promise is a handle to the next event in the stream, plus all events following. 27 | // A Promise is effectively immutable and can be shared. To be used concurrently with Publish operations. 28 | type Promise[T any] interface { 29 | // Ready returns the ready channel for this node; the channel closes when this Promise is ready. 30 | Ready() <-chan struct{} 31 | 32 | // Next returns the next event in the stream, and the next Promise. Multiple calls return consistent results. 33 | // Returns (zero, nil) when the stream is Closed. 34 | // 35 | // Note that this method internally blocks until the Ready() channel is closed! 36 | // Typical callers will not call Next() until this Promise is ready. 37 | Next() (T, Promise[T]) 38 | 39 | // Iterator creates an Iterator based on this Promise. The Promise is unchanged. 40 | Iterator() Iterator[T] 41 | } 42 | 43 | // ErrDone is returned by Iterator.Next() when the underlying EventStream is closed. 44 | var ErrDone = errors.New("no more items in iterator") 45 | 46 | // Iterator iterates an event stream. To be used concurrently with Publish operations. 47 | // 48 | // Unlike Promises, Iterators are stateful and should not be shared across go routines. 49 | type Iterator[T any] interface { 50 | // Next returns the next event in the stream. 51 | // - Returns (, nil) when the next event is published. 52 | // - Returns (zero, ErrDone) when the stream is exhausted. 53 | // - Returns (zero, ctx.Err()) if the context is cancelled. 54 | // Blocks until one of these three outcomes occurs. 55 | Next(ctx context.Context) (T, error) 56 | 57 | // Consume iterates the remainder of the stream, calling the provided callback with each successive value. 58 | // - Returns `nil` when the stream is exhausted, or if the callback returns ErrDone. 59 | // - Returns `` if the callback returns any other non-nil error. 60 | // - Returns `ctx.Err()` if the context is cancelled. 61 | // Blocks until one of these three outcomes occurs. 62 | Consume(ctx context.Context, callback func(context.Context, T) error) error 63 | } 64 | -------------------------------------------------------------------------------- /errgroup/ctxgroup.go: -------------------------------------------------------------------------------- 1 | package errgroup 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | type ctxGroup struct { 9 | ctx context.Context 10 | cancel func(error) 11 | 12 | wg sync.WaitGroup 13 | 14 | sem chan token 15 | 16 | errOnce sync.Once 17 | err error 18 | } 19 | 20 | var _ ContextGroup = (*ctxGroup)(nil) 21 | 22 | func (g *ctxGroup) done() { 23 | if g.sem != nil { 24 | <-g.sem 25 | } 26 | g.wg.Done() 27 | } 28 | 29 | // New returns a new ContextGroup derived from ctx. 30 | // 31 | // All funcs passed into [ContextGroup.Go] are wrapped with panic handlers and receive the 32 | // group context automatically. 33 | func New(ctx context.Context) ContextGroup { 34 | ctx, cancel := context.WithCancelCause(ctx) 35 | return &ctxGroup{ctx: ctx, cancel: cancel} 36 | } 37 | 38 | // Wait blocks until all function calls from the Go method have returned, then returns the first non-nil error (if any) from them. 39 | func (g *ctxGroup) Wait() error { 40 | g.wg.Wait() 41 | g.cancel(g.err) 42 | return g.err 43 | } 44 | 45 | // Go calls the given function in a new goroutine. 46 | func (g *ctxGroup) Go(f func(context.Context) error) { 47 | if g.sem != nil { 48 | select { 49 | case <-g.ctx.Done(): 50 | g.error(g.ctx.Err()) 51 | return 52 | case g.sem <- token{}: 53 | } 54 | } 55 | 56 | if err := g.ctx.Err(); err != nil { 57 | g.error(err) 58 | return 59 | } 60 | 61 | g.wg.Add(1) 62 | go func() { 63 | defer g.done() 64 | panicked := true 65 | defer func() { 66 | if panicked { 67 | g.error(NewPanicErrorCallers(recover(), 2)) 68 | } 69 | }() 70 | err := f(g.ctx) 71 | panicked = false 72 | if err != nil { 73 | g.error(err) 74 | } 75 | }() 76 | } 77 | 78 | func (g *ctxGroup) TryGo(f func(context.Context) error) bool { 79 | if g.sem != nil { 80 | select { 81 | case g.sem <- token{}: 82 | // Note: this allows barging iff channels in general allow barging. 83 | case <-g.ctx.Done(): 84 | g.error(g.ctx.Err()) 85 | return true 86 | default: 87 | return false 88 | } 89 | } 90 | 91 | if err := g.ctx.Err(); err != nil { 92 | g.error(err) 93 | return true 94 | } 95 | 96 | g.wg.Add(1) 97 | go func() { 98 | defer g.done() 99 | panicked := true 100 | defer func() { 101 | if panicked { 102 | g.error(NewPanicErrorCallers(recover(), 2)) 103 | } 104 | }() 105 | err := f(g.ctx) 106 | panicked = false 107 | if err != nil { 108 | g.error(err) 109 | } 110 | }() 111 | return true 112 | } 113 | 114 | func (g *ctxGroup) error(err error) { 115 | g.errOnce.Do(func() { 116 | g.err = err 117 | g.cancel(err) 118 | }) 119 | } 120 | 121 | type ctxGroupBuilder struct { 122 | limit int 123 | } 124 | 125 | func (b ctxGroupBuilder) New(ctx context.Context) ContextGroup { 126 | ctx, cancel := context.WithCancelCause(ctx) 127 | var sem chan token 128 | if b.limit >= 0 { 129 | sem = make(chan token, b.limit) 130 | } 131 | return &ctxGroup{ctx: ctx, cancel: cancel, sem: sem} 132 | } 133 | 134 | // WithLimit begins creating a New ContextGroup which limits the number of 135 | // active goroutines in this group to at most n. A negative value indicates no limit. 136 | func WithLimit(limit int) ctxGroupBuilder { 137 | return ctxGroupBuilder{limit: limit} 138 | } 139 | -------------------------------------------------------------------------------- /errgroup/panic_test.go: -------------------------------------------------------------------------------- 1 | package errgroup 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "runtime" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestPanicError(t *testing.T) { 12 | err := NewPanicError("test panic") 13 | for _, tc := range []struct { 14 | fmt string 15 | want string 16 | }{ 17 | {"%v", `panic: test panic`}, 18 | {"%s", `panic: test panic`}, 19 | {"%q", `"panic: test panic`}, 20 | {"%#v", `&errgroup.PanicError{recovered:"test panic"`}, 21 | } { 22 | got := fmt.Sprintf(tc.fmt, err) 23 | if !strings.HasPrefix(got, tc.want) { 24 | t.Errorf("got: %q, want: %q", got, tc.want) 25 | } 26 | } 27 | 28 | // test %+v manually 29 | got := fmt.Sprintf("%+v", err) 30 | lines := strings.Split(got, "\n") 31 | if lines[0] != "panic: test panic [recovered]" { 32 | t.Error(lines[0]) 33 | } 34 | if lines[1] != "" { 35 | t.Error(lines[1]) 36 | } 37 | if !strings.HasPrefix(lines[2], "goroutine") { 38 | t.Error("expected goroutine stack trace") 39 | } 40 | // lines 1 and 2 contain the explicit call to panic 41 | if !strings.Contains(lines[3], "errgroup.TestPanicError") { 42 | t.Error("expected errgroup.TestPanicError") 43 | } 44 | if !strings.Contains(lines[4], "errgroup/panic_test.go") { 45 | t.Error("expected errgroup/panic_test.go") 46 | } 47 | } 48 | 49 | func TestPanicErrorIntegration(t *testing.T) { 50 | var g Group 51 | g.Go(func() error { 52 | panic("test panic") 53 | }) 54 | var pe *PanicError 55 | err := g.Wait() 56 | if !errors.As(err, &pe) { 57 | t.Fatal("expected panic error") 58 | } 59 | trace := pe.StackTrace() 60 | lines := strings.Split(trace, "\n") 61 | if !strings.HasPrefix(lines[0], "goroutine") { 62 | t.Error("expected goroutine stack trace") 63 | } 64 | // lines 1 and 2 contain the explicit call to panic 65 | if !strings.Contains(lines[1], "panic(") { 66 | t.Error("expected panic(") 67 | } 68 | if !strings.Contains(lines[2], "runtime/panic.go") { 69 | t.Error("expected runtime/panic.go") 70 | } 71 | // lines 3 and 4 is our func 72 | if !strings.Contains(lines[3], "errgroup.TestPanicErrorIntegration.func1()") { 73 | t.Error("expected errgroup.TestPanicErrorIntegration.func1()") 74 | } 75 | if !strings.Contains(lines[4], "errgroup/panic_test.go") { 76 | t.Error("expected errgroup/panic_test.go") 77 | } 78 | } 79 | 80 | func TestPanicErrorStackTrace(t *testing.T) { 81 | err := NewPanicError("test panic") 82 | trace := err.StackTrace() 83 | lines := strings.Split(trace, "\n") 84 | if !strings.HasPrefix(lines[0], "goroutine") { 85 | t.Error("expected goroutine stack trace") 86 | } 87 | if !strings.Contains(lines[1], "errgroup.TestPanicErrorStackTrace") { 88 | t.Error("expected errgroup.TestPanicErrorStackTrace") 89 | } 90 | if !strings.Contains(lines[2], "errgroup/panic_test.go") { 91 | t.Error("expected errgroup/panic_test.go") 92 | } 93 | } 94 | 95 | func TestPanicErrorStackFrames(t *testing.T) { 96 | err := NewPanicError("test panic") 97 | first, _ := runtime.CallersFrames(err.StackFrames()).Next() 98 | const want = `github.com/fullstorydev/go/errgroup.TestPanicErrorStackFrames` 99 | if got := first.Function; got != want { 100 | t.Errorf("got: %q, want: %q", got, want) 101 | } 102 | } 103 | 104 | func TestPanicErrorStackFramesCallers(t *testing.T) { 105 | err := NewPanicErrorCallers("test panic", 0) 106 | first, _ := runtime.CallersFrames(err.StackFrames()).Next() 107 | const want = `github.com/fullstorydev/go/errgroup.NewPanicErrorCallers` 108 | if got := first.Function; got != want { 109 | t.Errorf("got: %q, want: %q", got, want) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /errgroup/errgroup.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the [LICENSE.google] file. 4 | // 5 | // Forked from v0.11.0 - https://go.googlesource.com/sync/+/refs/tags/v0.11.0/LICENSE 6 | 7 | package errgroup 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | "sync" 13 | ) 14 | 15 | type token struct{} 16 | 17 | // Group is a drop-in replacement for [golang.org/x/sync/errgroup.Group] that automatically catches panics. 18 | // Provided for backwards compatibility; prefer New() [ContextGroup] instead. 19 | type Group struct { 20 | cancel func(error) 21 | 22 | wg sync.WaitGroup 23 | 24 | sem chan token 25 | 26 | errOnce sync.Once 27 | err error 28 | } 29 | 30 | func (g *Group) done() { 31 | if g.sem != nil { 32 | <-g.sem 33 | } 34 | g.wg.Done() 35 | } 36 | 37 | // WithContext returns a new Group derived from ctx. 38 | // 39 | // All funcs passed into [Group.Go] are wrapped with panic handlers. 40 | func WithContext(ctx context.Context) (*Group, context.Context) { 41 | ctx, cancel := context.WithCancelCause(ctx) 42 | return &Group{cancel: cancel}, ctx 43 | } 44 | 45 | // Wait blocks until all function calls from the Go method have returned, then 46 | // returns the first non-nil error (if any) from them. 47 | func (g *Group) Wait() error { 48 | g.wg.Wait() 49 | if g.cancel != nil { 50 | g.cancel(g.err) 51 | } 52 | return g.err 53 | } 54 | 55 | // Go calls the given function in a new goroutine, passing the group context. 56 | // 57 | // The first call to return a non-nil error cancels the group's context. 58 | // The error will be returned by Wait. 59 | func (g *Group) Go(f func() error) { 60 | if g.sem != nil { 61 | g.sem <- token{} 62 | } 63 | 64 | g.wg.Add(1) 65 | go func() { 66 | defer g.done() 67 | panicked := true 68 | defer func() { 69 | if panicked { 70 | g.error(NewPanicErrorCallers(recover(), 2)) 71 | } 72 | }() 73 | err := f() 74 | panicked = false 75 | if err != nil { 76 | g.error(err) 77 | } 78 | }() 79 | } 80 | 81 | // TryGo calls the given function in a new goroutine only if the number of 82 | // active goroutines in the group is currently below the configured limit. 83 | // 84 | // The return value reports whether the goroutine was started. 85 | func (g *Group) TryGo(f func() error) bool { 86 | if g.sem != nil { 87 | select { 88 | case g.sem <- token{}: 89 | // Note: this allows barging iff channels in general allow barging. 90 | default: 91 | return false 92 | } 93 | } 94 | 95 | g.wg.Add(1) 96 | go func() { 97 | defer g.done() 98 | panicked := true 99 | defer func() { 100 | if panicked { 101 | g.error(NewPanicErrorCallers(recover(), 2)) 102 | } 103 | }() 104 | err := f() 105 | panicked = false 106 | if err != nil { 107 | g.error(err) 108 | } 109 | }() 110 | return true 111 | } 112 | 113 | // SetLimit limits the number of active goroutines in this group to at most n. 114 | // A negative value indicates no limit. 115 | // 116 | // Any subsequent call to the Go method will block until it can add an active 117 | // goroutine without exceeding the configured limit. 118 | // 119 | // The limit must not be modified while any goroutines in the group are active. 120 | func (g *Group) SetLimit(n int) { 121 | if n < 0 { 122 | g.sem = nil 123 | return 124 | } 125 | if len(g.sem) != 0 { 126 | panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem))) 127 | } 128 | g.sem = make(chan token, n) 129 | } 130 | 131 | func (g *Group) error(err error) { 132 | g.errOnce.Do(func() { 133 | g.err = err 134 | if g.cancel != nil { 135 | g.cancel(err) 136 | } 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /eventstream/README.md: -------------------------------------------------------------------------------- 1 | # eventstream 2 | 3 | An `EventStream` is a fast, efficient way to publish and subscribe to events within a Go process. 4 | 5 | The primary difference between `EventStream` and Go's native `channel` is that with `EventStream`, 6 | all subscribers see _every_ event. (With Go native channels, only one concurrent reader can get 7 | any particular event). 8 | 9 | Another difference from Go channels is that multiple concurrent publishers must be externally 10 | synchronized. (With Go channels, multiple concurrent publishers are internally synchronized.) 11 | We chose to avoid internal synchronization on `Publish` so that the common case of a single 12 | publisher would be maximally efficient. 13 | 14 | Event values passed through `EventStream` are opaque to `EventStream`. You must ensure these 15 | values are either effectively immutable or else correctly synchronized, since multiple 16 | subscribers will receive references to the same value. 17 | 18 | ## How it works 19 | 20 | `EventStream` is an immutable, append-only linked list of nodes. The publishing side keeps 21 | a reference only to the end of the list-- a single tail node that is not yet ready. During 22 | a publish operation, a new tail is created and linked from the current tail, and then the 23 | current tail is "made ready" by closing its channel, which signals all subscribers so they 24 | can read the next published value. 25 | 26 | Subscribers keep a reference to the next node in the linked list that they need to consume. 27 | As subscribers traverse the linked list, the head of the list becomes unreferenced and 28 | available for garbage collection (GC). 29 | 30 | In practice, `EventStream` uses an internal buffer of nodes to avoid frequent allocations, 31 | so GC of older events may be delayed until a sufficient number of new events have passed through. 32 | If your events pin a lot of memory, you might want to use a small buffer size so that 33 | nodes can be collected more frequently. 34 | 35 | ## Use cases 36 | 37 | Use this wherever you might have used a Go channel, but you need to multiple subscribers to each 38 | receive all events. 39 | 40 | - Publish a shared stream of events to all connected clients (for an example, see [BWAMP](https://bwamp.me)) 41 | - Synchronize a shared data model across services using gRPC streams. 42 | 43 | ## Examples 44 | 45 | ### Basic 46 | 47 | ```go 48 | package main 49 | 50 | import ( 51 | "context" 52 | "errors" 53 | "log" 54 | "sync" 55 | "time" 56 | 57 | "github.com/fullstorydev/go/eventstream" 58 | ) 59 | 60 | func main() { 61 | ctx := context.Background() 62 | stream := eventstream.New[string]() 63 | 64 | var wg sync.WaitGroup 65 | defer wg.Wait() 66 | for i := 0; i < 3; i++ { 67 | i := i 68 | it := stream.Subscribe().Iterator() 69 | wg.Add(1) 70 | go func() { 71 | defer wg.Done() 72 | for { 73 | var v interface{} 74 | v, err = it.Next(ctx) 75 | if errors.Is(err, eventstream.ErrDone) { 76 | return 77 | } else if err != nil { 78 | panic(err) 79 | } 80 | log.Printf("%d: %s", i, v.(string)) 81 | } 82 | }() 83 | } 84 | 85 | stream.Publish("Hello!") 86 | time.Sleep(time.Second) 87 | stream.Publish("I am") 88 | time.Sleep(time.Second) 89 | stream.Publish("EventStream") 90 | time.Sleep(time.Second) 91 | stream.Close() 92 | } 93 | ``` 94 | 95 | Running this will produce something like: 96 | ``` 97 | 2021/12/16 13:41:14 0: Hello! 98 | 2021/12/16 13:41:14 1: Hello! 99 | 2021/12/16 13:41:14 2: Hello! 100 | 2021/12/16 13:41:15 0: I am 101 | 2021/12/16 13:41:15 1: I am 102 | 2021/12/16 13:41:15 2: I am 103 | 2021/12/16 13:41:16 0: EventStream 104 | 2021/12/16 13:41:16 2: EventStream 105 | 2021/12/16 13:41:16 1: EventStream 106 | ``` 107 | 108 | ### chatterbox 109 | 110 | See [chatterbox](../examples/chatterbox) for a full example chat client implemented using gRPC streams with `EventStream`. 111 | -------------------------------------------------------------------------------- /eventstream/test/eventstream_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "testing" 7 | 8 | "github.com/fullstorydev/go/eventstream" 9 | "golang.org/x/sync/errgroup" 10 | "gotest.tools/v3/assert" 11 | ) 12 | 13 | func TestEventStream_Serial(t *testing.T) { 14 | ctx, cancel := context.WithCancel(context.Background()) 15 | defer cancel() 16 | 17 | es := eventstream.NewWithBuffer[int](16) // small buffer so we exercise rolling over 18 | 19 | it1 := es.Subscribe().Iterator() // sees everything 20 | var it2 eventstream.Iterator[int] 21 | for i := 0; i < 100; i++ { 22 | if i == 50 { 23 | it2 = es.Subscribe().Iterator() // sees 50 - 99 24 | } 25 | es.Publish(i) 26 | } 27 | it3 := es.Subscribe().Iterator() // sees nothing 28 | es.Close() 29 | it4 := es.Subscribe().Iterator() // subscribe after close sees nothing 30 | 31 | for i := 0; i < 100; i++ { 32 | v, err := it1.Next(ctx) 33 | assert.NilError(t, err, "should not err") 34 | assert.Equal(t, i, v, "wrong") 35 | } 36 | assertDone(ctx, t, it1) 37 | 38 | for i := 50; i < 100; i++ { 39 | v, err := it2.Next(ctx) 40 | assert.NilError(t, err, "should not err") 41 | assert.Equal(t, i, v, "wrong") 42 | } 43 | assertDone(ctx, t, it2) 44 | 45 | assertDone(ctx, t, it3) 46 | assertDone(ctx, t, it4) 47 | } 48 | 49 | func TestEventStream_Concurrent(t *testing.T) { 50 | ctx, cancel := context.WithCancel(context.Background()) 51 | defer cancel() 52 | g, ctx := errgroup.WithContext(ctx) 53 | 54 | es := eventstream.NewWithBuffer[int](16) // small buffer so we exercise rolling over 55 | 56 | itEverything := es.Subscribe().Iterator() // sees everything 57 | g.Go(func() error { 58 | for i := 0; i < 100; i++ { 59 | v, err := itEverything.Next(ctx) 60 | assert.NilError(t, err) 61 | assert.Equal(t, i, v) 62 | } 63 | assertDone(ctx, t, itEverything) 64 | return nil 65 | }) 66 | 67 | for i := 0; i < 100; i++ { 68 | if i == 50 { 69 | itHalf := es.Subscribe().Iterator() // sees 50 - 99 70 | g.Go(func() error { 71 | for i := 50; i < 100; i++ { 72 | v, err := itHalf.Next(ctx) 73 | assert.NilError(t, err) 74 | assert.Equal(t, i, v) 75 | } 76 | assertDone(ctx, t, itHalf) 77 | return nil 78 | }) 79 | } 80 | es.Publish(i) 81 | } 82 | itNone := es.Subscribe().Iterator() // sees nothing 83 | g.Go(func() error { 84 | assertDone(ctx, t, itNone) 85 | return nil 86 | }) 87 | es.Close() 88 | itClosed := es.Subscribe().Iterator() // subscribe after close sees nothing 89 | g.Go(func() error { 90 | assertDone(ctx, t, itClosed) 91 | return nil 92 | }) 93 | 94 | assert.NilError(t, g.Wait()) 95 | } 96 | 97 | func assertDone(ctx context.Context, t *testing.T, p eventstream.Iterator[int]) { 98 | t.Helper() 99 | v, err := p.Next(ctx) 100 | assert.Assert(t, v == 0) 101 | assert.Equal(t, eventstream.ErrDone, err) 102 | } 103 | 104 | func TestEventStream_IteratorConsume(t *testing.T) { 105 | es := eventstream.New[int]() 106 | prom := es.Subscribe() 107 | es.Publish(1) 108 | es.Publish(2) 109 | es.Publish(3) 110 | es.Close() 111 | 112 | var collect []int 113 | goodCollector := func(ctx context.Context, v int) error { 114 | collect = append(collect, v) 115 | return nil 116 | } 117 | 118 | errCollector := func(err error) func(ctx context.Context, v int) error { 119 | return func(ctx context.Context, v int) error { 120 | return err 121 | } 122 | } 123 | 124 | for _, tc := range []struct { 125 | name string 126 | collector func(ctx context.Context, v int) error 127 | expect []int 128 | expectErr error 129 | }{ 130 | {"good", goodCollector, []int{1, 2, 3}, nil}, 131 | {"abort", errCollector(eventstream.ErrDone), nil, nil}, 132 | {"error", errCollector(io.EOF), nil, io.EOF}, 133 | {"cancel", goodCollector, nil, context.Canceled}, 134 | } { 135 | t.Run(tc.name, func(t *testing.T) { 136 | ctx, cancel := context.WithCancel(context.Background()) 137 | defer cancel() 138 | 139 | if tc.name == "cancel" { 140 | cancel() 141 | } 142 | 143 | collect = nil 144 | err := prom.Iterator().Consume(ctx, tc.collector) 145 | if tc.expectErr == nil { 146 | assert.NilError(t, err) 147 | } else { 148 | assert.ErrorType(t, err, tc.expectErr) 149 | } 150 | assert.DeepEqual(t, tc.expect, collect) 151 | }) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /examples/chatterbox/chatclient/client.go: -------------------------------------------------------------------------------- 1 | package chatclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sync" 8 | "time" 9 | 10 | "github.com/fullstorydev/go/examples/chatterbox" 11 | ) 12 | 13 | // RunClient is an example of a gRPC client for a two-way bidi stream. 14 | func RunClient(ctx context.Context, chatInput <-chan string, cl chatterbox.ChatterBoxClient) error { 15 | mc := &MembersClient{ 16 | cl: cl, 17 | chatInput: chatInput, 18 | } 19 | 20 | if err := mc.Start(ctx); err != nil { 21 | return fmt.Errorf("failed to start client: %w", err) 22 | } 23 | 24 | <-ctx.Done() // block forever for this sample 25 | return nil 26 | } 27 | 28 | type MembersClient struct { 29 | cl chatterbox.ChatterBoxClient 30 | chatInput <-chan string 31 | 32 | mu sync.RWMutex 33 | members chatterbox.MembersModel 34 | } 35 | 36 | // Start this MembersClient. Fetches the initial state synchronously, then background monitors until ctx is cancelled. 37 | func (mc *MembersClient) Start(ctx context.Context) error { 38 | started := false 39 | ctx, cancel := context.WithCancel(ctx) 40 | defer func() { 41 | // Ensure cleanup happens if we do not start successfully. 42 | if !started { 43 | cancel() 44 | } 45 | }() 46 | 47 | // Synchronously ensure we can fetch an initial model before we return. 48 | stream, err := mc.startStream(ctx) 49 | if err != nil { 50 | return err // failed to fetch the initial state 51 | } 52 | started = true 53 | 54 | go func() { 55 | // Monitor the first stream until it dies. 56 | if err := mc.monitorStream(ctx, stream); err != nil { 57 | log.Println(err) 58 | } 59 | if ctx.Err() != nil { 60 | return 61 | } 62 | // Run until ctx is cancelled. 63 | if err := mc.Run(ctx); err != nil { 64 | log.Println(err) 65 | } 66 | }() 67 | return nil 68 | } 69 | 70 | // Run runs this MembersClient in the foreground until ctx is cancelled. 71 | func (mc *MembersClient) Run(ctx context.Context) error { 72 | const maxBackoff = 16 * time.Second 73 | backoff := time.Second 74 | 75 | // Loop forever until killed or cancelled. 76 | for { 77 | ok, err := func() (bool, error) { 78 | ctx, cancel := context.WithCancel(ctx) 79 | defer cancel() 80 | 81 | stream, err := mc.startStream(ctx) 82 | if err != nil { 83 | return false, err 84 | } 85 | 86 | return true, mc.monitorStream(ctx, stream) 87 | }() 88 | if err != nil { 89 | log.Println(err) 90 | } 91 | 92 | if ok { 93 | backoff = time.Second 94 | } else { 95 | backoff *= 2 96 | if backoff > maxBackoff { 97 | backoff = maxBackoff 98 | } 99 | } 100 | 101 | select { 102 | case <-ctx.Done(): 103 | return nil 104 | case <-time.After(backoff): 105 | } 106 | } 107 | } 108 | 109 | func (mc *MembersClient) startStream(ctx context.Context) (chatterbox.ChatterBox_ChatClient, error) { 110 | stream, err := mc.cl.Chat(ctx) 111 | if err != nil { 112 | return nil, fmt.Errorf("cl.Chat: %w", err) 113 | } 114 | 115 | members, err := fetchInitialState(ctx, stream) 116 | if err != nil { 117 | return nil, fmt.Errorf("fetchInitialState: %w", err) 118 | } 119 | 120 | // Successfully fetched initial state. 121 | log.Printf("Members: %+v", members) 122 | func() { 123 | mc.mu.Lock() 124 | defer mc.mu.Unlock() 125 | mc.members = members 126 | }() 127 | return stream, nil 128 | } 129 | 130 | // monitor pulls from the given stream until it closes, updating members model. 131 | func (mc *MembersClient) monitorStream(ctx context.Context, stream chatterbox.ChatterBox_ChatClient) error { 132 | var wg sync.WaitGroup 133 | defer wg.Wait() 134 | 135 | // Whenever our stream is up, send any chat inputs to the server. 136 | ctx, cancel := context.WithCancel(ctx) 137 | defer cancel() 138 | 139 | wg.Add(1) 140 | go func() { 141 | defer wg.Done() 142 | 143 | for { 144 | select { 145 | case <-ctx.Done(): 146 | return 147 | case msg, ok := <-mc.chatInput: 148 | if !ok { 149 | return 150 | } 151 | if err := stream.Send(&chatterbox.Send{ 152 | Text: msg, 153 | }); err != nil { 154 | log.Printf("Failed to send: %s", err) 155 | return 156 | } 157 | } 158 | } 159 | }() 160 | 161 | // Monitor the connection. 162 | for { 163 | msg, err := stream.Recv() 164 | if err != nil { 165 | return filterClientError(err) 166 | } 167 | 168 | switch msg.What { 169 | case chatterbox.What_CHAT: 170 | log.Printf("%s: %s", msg.Who, msg.Text) 171 | case chatterbox.What_JOIN: 172 | func() { 173 | mc.mu.Lock() 174 | defer mc.mu.Unlock() 175 | mc.members.Add(msg.Who) 176 | }() 177 | log.Printf("%s: joined", msg.Who) 178 | case chatterbox.What_LEAVE: 179 | func() { 180 | mc.mu.Lock() 181 | defer mc.mu.Unlock() 182 | mc.members.Remove(msg.Who) 183 | }() 184 | log.Printf("%s: left", msg.Who) 185 | default: 186 | return fmt.Errorf("unexpected type: %s", msg.What) 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /examples/chatterbox/chatclient/monitor.go: -------------------------------------------------------------------------------- 1 | package chatclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sync" 8 | "time" 9 | 10 | "github.com/fullstorydev/go/examples/chatterbox" 11 | "google.golang.org/protobuf/types/known/emptypb" 12 | ) 13 | 14 | const ( 15 | // Because clients run infinite retry loops, they should exponentially backoff when servers are unavailable. 16 | // This might be much higher in a real system 17 | maxBackoff = 8 * time.Second 18 | minBackoff = time.Second 19 | ) 20 | 21 | // RunMonitor is an example of a gRPC client for a one-way server stream. 22 | func RunMonitor(ctx context.Context, cl chatterbox.ChatterBoxClient) error { 23 | mm := &MembersMonitor{ 24 | cl: cl, 25 | } 26 | 27 | if err := mm.Start(ctx); err != nil { 28 | return fmt.Errorf("failed to start monitor: %w", err) 29 | } 30 | 31 | // Every 10 seconds, print the current member list. 32 | ticker := time.NewTicker(10 * time.Second) 33 | defer ticker.Stop() 34 | for { 35 | log.Printf("Members: %s", mm.GetMembers()) 36 | 37 | select { 38 | case <-ctx.Done(): 39 | return nil 40 | case <-ticker.C: 41 | } 42 | } 43 | } 44 | 45 | type MembersMonitor struct { 46 | cl chatterbox.ChatterBoxClient 47 | 48 | mu sync.RWMutex 49 | members chatterbox.MembersModel 50 | } 51 | 52 | // GetMembers returns the current list of members (at all times) to the rest of the application. 53 | func (mm *MembersMonitor) GetMembers() []string { 54 | mm.mu.RLock() 55 | defer mm.mu.RUnlock() 56 | return mm.members.Strings() 57 | } 58 | 59 | // Start this MembersMonitor. Fetches the initial state synchronously, then background monitors until ctx is cancelled. 60 | func (mm *MembersMonitor) Start(ctx context.Context) error { 61 | started := false 62 | ctx, cancel := context.WithCancel(ctx) 63 | defer func() { 64 | // Ensure cleanup happens if we do not start successfully. 65 | if !started { 66 | cancel() 67 | } 68 | }() 69 | 70 | // Synchronously ensure we can fetch an initial model before we return. 71 | stream, err := mm.startStream(ctx) 72 | if err != nil { 73 | return err // failed to fetch the initial state 74 | } 75 | started = true 76 | 77 | go func() { 78 | // Monitor the first stream until it dies. 79 | if err := mm.monitorStream(ctx, stream); err != nil { 80 | log.Println(err) 81 | } 82 | if ctx.Err() != nil { 83 | return 84 | } 85 | // Run until ctx is cancelled. 86 | if err := mm.Run(ctx); err != nil { 87 | log.Println(err) 88 | } 89 | }() 90 | return nil 91 | } 92 | 93 | // Run runs this MembersMonitor in the foreground until ctx is cancelled. 94 | func (mm *MembersMonitor) Run(ctx context.Context) error { 95 | backoff := minBackoff 96 | 97 | // Loop forever until killed or cancelled. 98 | for { 99 | ok, err := func() (bool, error) { 100 | ctx, cancel := context.WithCancel(ctx) 101 | defer cancel() 102 | 103 | stream, err := mm.startStream(ctx) 104 | if err != nil { 105 | return false, err 106 | } 107 | 108 | return true, mm.monitorStream(ctx, stream) 109 | }() 110 | if err != nil { 111 | log.Println(err) 112 | } 113 | 114 | if ok { 115 | backoff = minBackoff 116 | } else { 117 | backoff *= 2 118 | if backoff > maxBackoff { 119 | backoff = maxBackoff 120 | } 121 | } 122 | 123 | select { 124 | case <-ctx.Done(): 125 | return nil 126 | case <-time.After(backoff): 127 | } 128 | } 129 | } 130 | 131 | func (mm *MembersMonitor) startStream(ctx context.Context) (chatterbox.ChatterBox_MonitorClient, error) { 132 | stream, err := mm.cl.Monitor(ctx, &emptypb.Empty{}) 133 | if err != nil { 134 | return nil, fmt.Errorf("cl.Monitor: %w", err) 135 | } 136 | 137 | members, err := fetchInitialState(ctx, stream) 138 | if err != nil { 139 | return nil, fmt.Errorf("fetchInitialState: %w", err) 140 | } 141 | 142 | // Successfully fetched initial state. 143 | func() { 144 | mm.mu.Lock() 145 | defer mm.mu.Unlock() 146 | mm.members = members 147 | }() 148 | return stream, nil 149 | } 150 | 151 | // monitorStream pulls from the given stream until it closes, updating the members model. 152 | func (mm *MembersMonitor) monitorStream(_ context.Context, stream chatterbox.ChatterBox_MonitorClient) error { 153 | // Monitor the connection. 154 | for { 155 | msg, err := stream.Recv() 156 | if err != nil { 157 | return filterClientError(err) 158 | } 159 | 160 | switch msg.What { 161 | case chatterbox.What_CHAT: 162 | log.Printf("%s: %s", msg.Who, msg.Text) 163 | case chatterbox.What_JOIN: 164 | func() { 165 | mm.mu.Lock() 166 | defer mm.mu.Unlock() 167 | mm.members.Add(msg.Who) 168 | }() 169 | log.Printf("%s: joined", msg.Who) 170 | case chatterbox.What_LEAVE: 171 | func() { 172 | mm.mu.Lock() 173 | defer mm.mu.Unlock() 174 | mm.members.Remove(msg.Who) 175 | }() 176 | log.Printf("%s: left", msg.Who) 177 | default: 178 | return fmt.Errorf("unexpected type: %s", msg.What) 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /examples/chatterbox/chatterbox_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package chatterbox 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | emptypb "google.golang.org/protobuf/types/known/emptypb" 11 | ) 12 | 13 | // This is a compile-time assertion to ensure that this generated file 14 | // is compatible with the grpc package it is being compiled against. 15 | // Requires gRPC-Go v1.32.0 or later. 16 | const _ = grpc.SupportPackageIsVersion7 17 | 18 | // ChatterBoxClient is the client API for ChatterBox service. 19 | // 20 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 21 | type ChatterBoxClient interface { 22 | // Chat joins the chat room and sends chat messages. 23 | Chat(ctx context.Context, opts ...grpc.CallOption) (ChatterBox_ChatClient, error) 24 | // Monitor passively monitors the room. 25 | Monitor(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (ChatterBox_MonitorClient, error) 26 | } 27 | 28 | type chatterBoxClient struct { 29 | cc grpc.ClientConnInterface 30 | } 31 | 32 | func NewChatterBoxClient(cc grpc.ClientConnInterface) ChatterBoxClient { 33 | return &chatterBoxClient{cc} 34 | } 35 | 36 | func (c *chatterBoxClient) Chat(ctx context.Context, opts ...grpc.CallOption) (ChatterBox_ChatClient, error) { 37 | stream, err := c.cc.NewStream(ctx, &ChatterBox_ServiceDesc.Streams[0], "/chatterbox.ChatterBox/Chat", opts...) 38 | if err != nil { 39 | return nil, err 40 | } 41 | x := &chatterBoxChatClient{stream} 42 | return x, nil 43 | } 44 | 45 | type ChatterBox_ChatClient interface { 46 | Send(*Send) error 47 | Recv() (*Event, error) 48 | grpc.ClientStream 49 | } 50 | 51 | type chatterBoxChatClient struct { 52 | grpc.ClientStream 53 | } 54 | 55 | func (x *chatterBoxChatClient) Send(m *Send) error { 56 | return x.ClientStream.SendMsg(m) 57 | } 58 | 59 | func (x *chatterBoxChatClient) Recv() (*Event, error) { 60 | m := new(Event) 61 | if err := x.ClientStream.RecvMsg(m); err != nil { 62 | return nil, err 63 | } 64 | return m, nil 65 | } 66 | 67 | func (c *chatterBoxClient) Monitor(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (ChatterBox_MonitorClient, error) { 68 | stream, err := c.cc.NewStream(ctx, &ChatterBox_ServiceDesc.Streams[1], "/chatterbox.ChatterBox/Monitor", opts...) 69 | if err != nil { 70 | return nil, err 71 | } 72 | x := &chatterBoxMonitorClient{stream} 73 | if err := x.ClientStream.SendMsg(in); err != nil { 74 | return nil, err 75 | } 76 | if err := x.ClientStream.CloseSend(); err != nil { 77 | return nil, err 78 | } 79 | return x, nil 80 | } 81 | 82 | type ChatterBox_MonitorClient interface { 83 | Recv() (*Event, error) 84 | grpc.ClientStream 85 | } 86 | 87 | type chatterBoxMonitorClient struct { 88 | grpc.ClientStream 89 | } 90 | 91 | func (x *chatterBoxMonitorClient) Recv() (*Event, error) { 92 | m := new(Event) 93 | if err := x.ClientStream.RecvMsg(m); err != nil { 94 | return nil, err 95 | } 96 | return m, nil 97 | } 98 | 99 | // ChatterBoxServer is the server API for ChatterBox service. 100 | // All implementations must embed UnimplementedChatterBoxServer 101 | // for forward compatibility 102 | type ChatterBoxServer interface { 103 | // Chat joins the chat room and sends chat messages. 104 | Chat(ChatterBox_ChatServer) error 105 | // Monitor passively monitors the room. 106 | Monitor(*emptypb.Empty, ChatterBox_MonitorServer) error 107 | mustEmbedUnimplementedChatterBoxServer() 108 | } 109 | 110 | // UnimplementedChatterBoxServer must be embedded to have forward compatible implementations. 111 | type UnimplementedChatterBoxServer struct { 112 | } 113 | 114 | func (UnimplementedChatterBoxServer) Chat(ChatterBox_ChatServer) error { 115 | return status.Errorf(codes.Unimplemented, "method Chat not implemented") 116 | } 117 | func (UnimplementedChatterBoxServer) Monitor(*emptypb.Empty, ChatterBox_MonitorServer) error { 118 | return status.Errorf(codes.Unimplemented, "method Monitor not implemented") 119 | } 120 | func (UnimplementedChatterBoxServer) mustEmbedUnimplementedChatterBoxServer() {} 121 | 122 | // UnsafeChatterBoxServer may be embedded to opt out of forward compatibility for this service. 123 | // Use of this interface is not recommended, as added methods to ChatterBoxServer will 124 | // result in compilation errors. 125 | type UnsafeChatterBoxServer interface { 126 | mustEmbedUnimplementedChatterBoxServer() 127 | } 128 | 129 | func RegisterChatterBoxServer(s grpc.ServiceRegistrar, srv ChatterBoxServer) { 130 | s.RegisterService(&ChatterBox_ServiceDesc, srv) 131 | } 132 | 133 | func _ChatterBox_Chat_Handler(srv interface{}, stream grpc.ServerStream) error { 134 | return srv.(ChatterBoxServer).Chat(&chatterBoxChatServer{stream}) 135 | } 136 | 137 | type ChatterBox_ChatServer interface { 138 | Send(*Event) error 139 | Recv() (*Send, error) 140 | grpc.ServerStream 141 | } 142 | 143 | type chatterBoxChatServer struct { 144 | grpc.ServerStream 145 | } 146 | 147 | func (x *chatterBoxChatServer) Send(m *Event) error { 148 | return x.ServerStream.SendMsg(m) 149 | } 150 | 151 | func (x *chatterBoxChatServer) Recv() (*Send, error) { 152 | m := new(Send) 153 | if err := x.ServerStream.RecvMsg(m); err != nil { 154 | return nil, err 155 | } 156 | return m, nil 157 | } 158 | 159 | func _ChatterBox_Monitor_Handler(srv interface{}, stream grpc.ServerStream) error { 160 | m := new(emptypb.Empty) 161 | if err := stream.RecvMsg(m); err != nil { 162 | return err 163 | } 164 | return srv.(ChatterBoxServer).Monitor(m, &chatterBoxMonitorServer{stream}) 165 | } 166 | 167 | type ChatterBox_MonitorServer interface { 168 | Send(*Event) error 169 | grpc.ServerStream 170 | } 171 | 172 | type chatterBoxMonitorServer struct { 173 | grpc.ServerStream 174 | } 175 | 176 | func (x *chatterBoxMonitorServer) Send(m *Event) error { 177 | return x.ServerStream.SendMsg(m) 178 | } 179 | 180 | // ChatterBox_ServiceDesc is the grpc.ServiceDesc for ChatterBox service. 181 | // It's only intended for direct use with grpc.RegisterService, 182 | // and not to be introspected or modified (even as a copy) 183 | var ChatterBox_ServiceDesc = grpc.ServiceDesc{ 184 | ServiceName: "chatterbox.ChatterBox", 185 | HandlerType: (*ChatterBoxServer)(nil), 186 | Methods: []grpc.MethodDesc{}, 187 | Streams: []grpc.StreamDesc{ 188 | { 189 | StreamName: "Chat", 190 | Handler: _ChatterBox_Chat_Handler, 191 | ServerStreams: true, 192 | ClientStreams: true, 193 | }, 194 | { 195 | StreamName: "Monitor", 196 | Handler: _ChatterBox_Monitor_Handler, 197 | ServerStreams: true, 198 | }, 199 | }, 200 | Metadata: "chatterbox.proto", 201 | } 202 | -------------------------------------------------------------------------------- /errgroup/ctxgroup_test.go: -------------------------------------------------------------------------------- 1 | package errgroup_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "sync/atomic" 11 | "testing" 12 | "time" 13 | 14 | "github.com/fullstorydev/go/errgroup" 15 | ) 16 | 17 | // JustErrors illustrates the use of a Group in place of a sync.WaitGroup to 18 | // simplify goroutine counting and error handling. This example is derived from 19 | // the sync.WaitGroup example at https://golang.org/pkg/sync/#example_WaitGroup. 20 | func ExampleNew_justErrors() { 21 | g := errgroup.New(context.Background()) 22 | var urls = []string{ 23 | "http://www.golang.org/", 24 | "http://www.google.com/", 25 | "http://www.somestupidname.com/", 26 | } 27 | for _, url := range urls { 28 | // Launch a goroutine to fetch the URL. 29 | url := url // https://golang.org/doc/faq#closures_and_goroutines 30 | g.Go(func(ctx context.Context) error { 31 | // Fetch the URL. 32 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 33 | if err != nil { 34 | panic(err) 35 | } 36 | resp, err := http.DefaultClient.Do(req) 37 | if err == nil { 38 | _ = resp.Body.Close() 39 | } 40 | return err 41 | }) 42 | } 43 | // Wait for all HTTP fetches to complete. 44 | if err := g.Wait(); err == nil { 45 | fmt.Println("Successfully fetched all URLs.") 46 | } 47 | 48 | // Output: 49 | // Successfully fetched all URLs. 50 | } 51 | 52 | // Parallel illustrates the use of a Group for synchronizing a simple parallel 53 | // task: the "Google Search 2.0" function from 54 | // https://talks.golang.org/2012/concurrency.slide#46, augmented with a Context 55 | // and error-handling. 56 | func ExampleNew_parallel() { 57 | Google := func(ctx context.Context, query string) ([]Result, error) { 58 | g := errgroup.New(ctx) 59 | 60 | searches := []Search{Web, Image, Video} 61 | results := make([]Result, len(searches)) 62 | for i, search := range searches { 63 | i, search := i, search // https://golang.org/doc/faq#closures_and_goroutines 64 | g.Go(func(ctx context.Context) error { 65 | result, err := search(ctx, query) 66 | if err == nil { 67 | results[i] = result 68 | } 69 | return err 70 | }) 71 | } 72 | if err := g.Wait(); err != nil { 73 | return nil, err 74 | } 75 | return results, nil 76 | } 77 | 78 | results, err := Google(context.Background(), "golang") 79 | if err != nil { 80 | _, _ = fmt.Fprintln(os.Stderr, err) 81 | return 82 | } 83 | for _, result := range results { 84 | fmt.Println(result) 85 | } 86 | 87 | // Output: 88 | // web result for "golang" 89 | // image result for "golang" 90 | // video result for "golang" 91 | } 92 | 93 | func TestZeroNew(t *testing.T) { 94 | err1 := errors.New("errgroup_test: 1") 95 | err2 := errors.New("errgroup_test: 2") 96 | 97 | cases := []struct { 98 | errs []error 99 | }{ 100 | {errs: []error{}}, 101 | {errs: []error{nil}}, 102 | {errs: []error{err1}}, 103 | {errs: []error{err1, nil}}, 104 | {errs: []error{err1, nil, err2}}, 105 | } 106 | 107 | for _, tc := range cases { 108 | g := errgroup.New(context.Background()) 109 | 110 | var firstErr error 111 | for i, err := range tc.errs { 112 | err := err 113 | g.Go(func(ctx context.Context) error { return err }) 114 | 115 | if firstErr == nil && err != nil { 116 | firstErr = err 117 | } 118 | 119 | if gErr := g.Wait(); gErr != firstErr { 120 | t.Errorf("after %T.Go(func() error { return err }) for err in %v\n"+ 121 | "g.Wait() = %v; want %v", 122 | g, tc.errs[:i+1], err, firstErr) 123 | } 124 | } 125 | } 126 | } 127 | 128 | func TestNew(t *testing.T) { 129 | errDoom := errors.New("group_test: doomed") 130 | 131 | cases := []struct { 132 | errs []error 133 | want error 134 | }{ 135 | {want: nil}, 136 | {errs: []error{nil}, want: nil}, 137 | {errs: []error{errDoom}, want: errDoom}, 138 | {errs: []error{errDoom, nil}, want: errDoom}, 139 | } 140 | 141 | for _, tc := range cases { 142 | g := errgroup.New(context.Background()) 143 | 144 | for _, err := range tc.errs { 145 | err := err 146 | g.Go(func(ctx context.Context) error { return err }) 147 | } 148 | 149 | if err := g.Wait(); err != tc.want { 150 | t.Errorf("after %T.Go(func() error { return err }) for err in %v\n"+ 151 | "g.Wait() = %v; want %v", 152 | g, tc.errs, err, tc.want) 153 | } 154 | } 155 | } 156 | 157 | func TestNewTryGo(t *testing.T) { 158 | g := errgroup.WithLimit(42).New(context.Background()) 159 | n := 42 160 | ch := make(chan struct{}) 161 | fn := func(ctx context.Context) error { 162 | ch <- struct{}{} 163 | return nil 164 | } 165 | for i := 0; i < n; i++ { 166 | if !g.TryGo(fn) { 167 | t.Fatalf("TryGo should succeed but got fail at %d-th call.", i) 168 | } 169 | } 170 | if g.TryGo(fn) { 171 | t.Fatalf("TryGo is expected to fail but succeeded.") 172 | } 173 | go func() { 174 | for i := 0; i < n; i++ { 175 | <-ch 176 | } 177 | }() 178 | _ = g.Wait() 179 | 180 | // disabled: cannot reuse ContextGroup. 181 | /* 182 | if !g.TryGo(fn) { 183 | t.Fatalf("TryGo should success but got fail after all goroutines.") 184 | } 185 | go func() { <-ch }() 186 | g.Wait() 187 | */ 188 | 189 | // Switch limit. 190 | g = errgroup.WithLimit(1).New(context.Background()) 191 | if !g.TryGo(fn) { 192 | t.Fatalf("TryGo should success but got failed.") 193 | } 194 | if g.TryGo(fn) { 195 | t.Fatalf("TryGo should fail but succeeded.") 196 | } 197 | go func() { <-ch }() 198 | _ = g.Wait() 199 | 200 | // Block all calls. 201 | g = errgroup.WithLimit(0).New(context.Background()) 202 | for i := 0; i < 1<<10; i++ { 203 | if g.TryGo(fn) { 204 | t.Fatalf("TryGo should fail but got succeded.") 205 | } 206 | } 207 | _ = g.Wait() 208 | } 209 | 210 | func TestNewGoLimit(t *testing.T) { 211 | const limit = 10 212 | 213 | g := errgroup.WithLimit(limit).New(context.Background()) 214 | var active int32 215 | for i := 0; i <= 1<<10; i++ { 216 | g.Go(func(ctx context.Context) error { 217 | n := atomic.AddInt32(&active, 1) 218 | if n > limit { 219 | return fmt.Errorf("saw %d active goroutines; want ≤ %d", n, limit) 220 | } 221 | time.Sleep(1 * time.Microsecond) // Give other goroutines a chance to increment active. 222 | atomic.AddInt32(&active, -1) 223 | return nil 224 | }) 225 | } 226 | if err := g.Wait(); err != nil { 227 | t.Fatal(err) 228 | } 229 | } 230 | 231 | func TestNewPanic(t *testing.T) { 232 | t.Run("Go", func(t *testing.T) { 233 | g := errgroup.New(context.Background()) 234 | g.Go(func(ctx context.Context) error { 235 | panic("test panic") 236 | }) 237 | err := g.Wait() 238 | if err == nil { 239 | t.Fatalf("Wait should return an error") 240 | } 241 | if !strings.HasPrefix(err.Error(), "panic: test panic") { 242 | t.Fatalf("Error message mismatch: %v", err) 243 | } 244 | }) 245 | 246 | t.Run("TryGo", func(t *testing.T) { 247 | g := errgroup.WithLimit(1).New(context.Background()) 248 | g.TryGo(func(ctx context.Context) error { 249 | panic("test panic") 250 | }) 251 | err := g.Wait() 252 | if err == nil { 253 | t.Fatalf("Wait should return an error") 254 | } 255 | if !strings.HasPrefix(err.Error(), "panic: test panic") { 256 | t.Fatalf("Error message mismatch: %v", err) 257 | } 258 | }) 259 | } 260 | 261 | func TestNewGoExitsEarly(t *testing.T) { 262 | var counter atomic.Uint32 263 | 264 | fn := func(ctx context.Context) error { 265 | counter.Add(1) 266 | <-ctx.Done() // wait until cancelled 267 | return nil 268 | } 269 | 270 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 271 | defer cancel() 272 | g := errgroup.WithLimit(1).New(ctx) 273 | 274 | g.Go(fn) // this should succeed 275 | g.Go(fn) // this should get stuck, then cancelled 276 | 277 | _ = g.Wait() 278 | 279 | if count := counter.Load(); count != 1 { 280 | t.Fatalf("Counter should be 1, got %d", count) 281 | } 282 | } 283 | 284 | func BenchmarkNewGo(b *testing.B) { 285 | fn := func() {} 286 | g := errgroup.New(context.Background()) 287 | b.ResetTimer() 288 | b.ReportAllocs() 289 | for i := 0; i < b.N; i++ { 290 | g.Go(func(ctx context.Context) error { fn(); return nil }) 291 | } 292 | _ = g.Wait() 293 | } 294 | -------------------------------------------------------------------------------- /errgroup/errgroup_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the [LICENSE.google] file. 4 | // 5 | // Forked from v0.11.0 - https://go.googlesource.com/sync/+/refs/tags/v0.11.0/LICENSE 6 | 7 | package errgroup_test 8 | 9 | import ( 10 | "context" 11 | "errors" 12 | "fmt" 13 | "net/http" 14 | "os" 15 | "strings" 16 | "sync/atomic" 17 | "testing" 18 | "time" 19 | 20 | "github.com/fullstorydev/go/errgroup" 21 | ) 22 | 23 | var ( 24 | Web = fakeSearch("web") 25 | Image = fakeSearch("image") 26 | Video = fakeSearch("video") 27 | ) 28 | 29 | type Result string 30 | type Search func(ctx context.Context, query string) (Result, error) 31 | 32 | func fakeSearch(kind string) Search { 33 | return func(_ context.Context, query string) (Result, error) { 34 | return Result(fmt.Sprintf("%s result for %q", kind, query)), nil 35 | } 36 | } 37 | 38 | // JustErrors illustrates the use of a Group in place of a sync.WaitGroup to 39 | // simplify goroutine counting and error handling. This example is derived from 40 | // the sync.WaitGroup example at https://golang.org/pkg/sync/#example_WaitGroup. 41 | func ExampleGroup_justErrors() { 42 | g := new(errgroup.Group) 43 | var urls = []string{ 44 | "http://www.golang.org/", 45 | "http://www.google.com/", 46 | "http://www.somestupidname.com/", 47 | } 48 | for _, url := range urls { 49 | // Launch a goroutine to fetch the URL. 50 | url := url // https://golang.org/doc/faq#closures_and_goroutines 51 | g.Go(func() error { 52 | // Fetch the URL. 53 | resp, err := http.Get(url) 54 | if err == nil { 55 | _ = resp.Body.Close() 56 | } 57 | return err 58 | }) 59 | } 60 | // Wait for all HTTP fetches to complete. 61 | if err := g.Wait(); err == nil { 62 | fmt.Println("Successfully fetched all URLs.") 63 | } 64 | 65 | // Output: 66 | // Successfully fetched all URLs. 67 | } 68 | 69 | // Parallel illustrates the use of a Group for synchronizing a simple parallel 70 | // task: the "Google Search 2.0" function from 71 | // https://talks.golang.org/2012/concurrency.slide#46, augmented with a Context 72 | // and error-handling. 73 | func ExampleGroup_parallel() { 74 | Google := func(ctx context.Context, query string) ([]Result, error) { 75 | g, ctx := errgroup.WithContext(ctx) 76 | 77 | searches := []Search{Web, Image, Video} 78 | results := make([]Result, len(searches)) 79 | for i, search := range searches { 80 | i, search := i, search // https://golang.org/doc/faq#closures_and_goroutines 81 | g.Go(func() error { 82 | result, err := search(ctx, query) 83 | if err == nil { 84 | results[i] = result 85 | } 86 | return err 87 | }) 88 | } 89 | if err := g.Wait(); err != nil { 90 | return nil, err 91 | } 92 | return results, nil 93 | } 94 | 95 | results, err := Google(context.Background(), "golang") 96 | if err != nil { 97 | _, _ = fmt.Fprintln(os.Stderr, err) 98 | return 99 | } 100 | for _, result := range results { 101 | fmt.Println(result) 102 | } 103 | 104 | // Output: 105 | // web result for "golang" 106 | // image result for "golang" 107 | // video result for "golang" 108 | } 109 | 110 | func TestZeroGroup(t *testing.T) { 111 | err1 := errors.New("errgroup_test: 1") 112 | err2 := errors.New("errgroup_test: 2") 113 | 114 | cases := []struct { 115 | errs []error 116 | }{ 117 | {errs: []error{}}, 118 | {errs: []error{nil}}, 119 | {errs: []error{err1}}, 120 | {errs: []error{err1, nil}}, 121 | {errs: []error{err1, nil, err2}}, 122 | } 123 | 124 | for _, tc := range cases { 125 | g := new(errgroup.Group) 126 | 127 | var firstErr error 128 | for i, err := range tc.errs { 129 | err := err 130 | g.Go(func() error { return err }) 131 | 132 | if firstErr == nil && err != nil { 133 | firstErr = err 134 | } 135 | 136 | if gErr := g.Wait(); gErr != firstErr { 137 | t.Errorf("after %T.Go(func() error { return err }) for err in %v\n"+ 138 | "g.Wait() = %v; want %v", 139 | g, tc.errs[:i+1], err, firstErr) 140 | } 141 | } 142 | } 143 | } 144 | 145 | func TestWithContext(t *testing.T) { 146 | errDoom := errors.New("group_test: doomed") 147 | 148 | cases := []struct { 149 | errs []error 150 | want error 151 | }{ 152 | {want: nil}, 153 | {errs: []error{nil}, want: nil}, 154 | {errs: []error{errDoom}, want: errDoom}, 155 | {errs: []error{errDoom, nil}, want: errDoom}, 156 | } 157 | 158 | for _, tc := range cases { 159 | g, ctx := errgroup.WithContext(context.Background()) 160 | 161 | for _, err := range tc.errs { 162 | err := err 163 | g.Go(func() error { return err }) 164 | } 165 | 166 | if err := g.Wait(); err != tc.want { 167 | t.Errorf("after %T.Go(func() error { return err }) for err in %v\n"+ 168 | "g.Wait() = %v; want %v", 169 | g, tc.errs, err, tc.want) 170 | } 171 | 172 | canceled := false 173 | select { 174 | case <-ctx.Done(): 175 | canceled = true 176 | default: 177 | } 178 | if !canceled { 179 | t.Errorf("after %T.Go(func() error { return err }) for err in %v\n"+ 180 | "ctx.Done() was not closed", 181 | g, tc.errs) 182 | } 183 | } 184 | } 185 | 186 | func TestTryGo(t *testing.T) { 187 | g := &errgroup.Group{} 188 | n := 42 189 | g.SetLimit(42) 190 | ch := make(chan struct{}) 191 | fn := func() error { 192 | ch <- struct{}{} 193 | return nil 194 | } 195 | for i := 0; i < n; i++ { 196 | if !g.TryGo(fn) { 197 | t.Fatalf("TryGo should succeed but got fail at %d-th call.", i) 198 | } 199 | } 200 | if g.TryGo(fn) { 201 | t.Fatalf("TryGo is expected to fail but succeeded.") 202 | } 203 | go func() { 204 | for i := 0; i < n; i++ { 205 | <-ch 206 | } 207 | }() 208 | _ = g.Wait() 209 | 210 | if !g.TryGo(fn) { 211 | t.Fatalf("TryGo should success but got fail after all goroutines.") 212 | } 213 | go func() { <-ch }() 214 | _ = g.Wait() 215 | 216 | // Switch limit. 217 | g.SetLimit(1) 218 | if !g.TryGo(fn) { 219 | t.Fatalf("TryGo should success but got failed.") 220 | } 221 | if g.TryGo(fn) { 222 | t.Fatalf("TryGo should fail but succeeded.") 223 | } 224 | go func() { <-ch }() 225 | _ = g.Wait() 226 | 227 | // Block all calls. 228 | g.SetLimit(0) 229 | for i := 0; i < 1<<10; i++ { 230 | if g.TryGo(fn) { 231 | t.Fatalf("TryGo should fail but got succeded.") 232 | } 233 | } 234 | _ = g.Wait() 235 | } 236 | 237 | func TestGoLimit(t *testing.T) { 238 | const limit = 10 239 | 240 | g := &errgroup.Group{} 241 | g.SetLimit(limit) 242 | var active int32 243 | for i := 0; i <= 1<<10; i++ { 244 | g.Go(func() error { 245 | n := atomic.AddInt32(&active, 1) 246 | if n > limit { 247 | return fmt.Errorf("saw %d active goroutines; want ≤ %d", n, limit) 248 | } 249 | time.Sleep(1 * time.Microsecond) // Give other goroutines a chance to increment active. 250 | atomic.AddInt32(&active, -1) 251 | return nil 252 | }) 253 | } 254 | if err := g.Wait(); err != nil { 255 | t.Fatal(err) 256 | } 257 | } 258 | 259 | func TestPanic(t *testing.T) { 260 | t.Run("Go", func(t *testing.T) { 261 | g := &errgroup.Group{} 262 | g.Go(func() error { 263 | panic("test panic") 264 | }) 265 | err := g.Wait() 266 | if err == nil { 267 | t.Fatalf("Wait should return an error") 268 | } 269 | if !strings.HasPrefix(err.Error(), "panic: test panic") { 270 | t.Fatalf("Error message mismatch: %v", err) 271 | } 272 | }) 273 | 274 | t.Run("TryGo", func(t *testing.T) { 275 | g := &errgroup.Group{} 276 | g.SetLimit(1) 277 | g.TryGo(func() error { 278 | panic("test panic") 279 | }) 280 | err := g.Wait() 281 | if err == nil { 282 | t.Fatalf("Wait should return an error") 283 | } 284 | if !strings.HasPrefix(err.Error(), "panic: test panic") { 285 | t.Fatalf("Error message mismatch: %v", err) 286 | } 287 | }) 288 | } 289 | 290 | func TestGoExitsEarly(t *testing.T) { 291 | var counter atomic.Uint32 292 | 293 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 294 | defer cancel() 295 | 296 | fn := func() error { 297 | if ctx.Err() != nil { 298 | return ctx.Err() 299 | } 300 | counter.Add(1) 301 | <-ctx.Done() // wait until cancelled 302 | return nil 303 | } 304 | 305 | g, _ := errgroup.WithContext(ctx) 306 | g.SetLimit(1) 307 | 308 | g.Go(fn) // this should succeed 309 | g.Go(fn) // this should get stuck, then cancelled 310 | 311 | _ = g.Wait() 312 | 313 | if count := counter.Load(); count != 1 { 314 | t.Fatalf("Counter should be 1, got %d", count) 315 | } 316 | } 317 | 318 | func BenchmarkGo(b *testing.B) { 319 | fn := func() {} 320 | g := &errgroup.Group{} 321 | b.ResetTimer() 322 | b.ReportAllocs() 323 | for i := 0; i < b.N; i++ { 324 | g.Go(func() error { fn(); return nil }) 325 | } 326 | _ = g.Wait() 327 | } 328 | -------------------------------------------------------------------------------- /examples/chatterbox/chatterbox.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v3.17.3 5 | // source: chatterbox.proto 6 | 7 | package chatterbox 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | emptypb "google.golang.org/protobuf/types/known/emptypb" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | type What int32 25 | 26 | const ( 27 | What_INITIALIZED What = 0 // signals that the client is fully initialized 28 | What_CHAT What = 1 29 | What_JOIN What = 2 30 | What_LEAVE What = 3 31 | ) 32 | 33 | // Enum value maps for What. 34 | var ( 35 | What_name = map[int32]string{ 36 | 0: "INITIALIZED", 37 | 1: "CHAT", 38 | 2: "JOIN", 39 | 3: "LEAVE", 40 | } 41 | What_value = map[string]int32{ 42 | "INITIALIZED": 0, 43 | "CHAT": 1, 44 | "JOIN": 2, 45 | "LEAVE": 3, 46 | } 47 | ) 48 | 49 | func (x What) Enum() *What { 50 | p := new(What) 51 | *p = x 52 | return p 53 | } 54 | 55 | func (x What) String() string { 56 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 57 | } 58 | 59 | func (What) Descriptor() protoreflect.EnumDescriptor { 60 | return file_chatterbox_proto_enumTypes[0].Descriptor() 61 | } 62 | 63 | func (What) Type() protoreflect.EnumType { 64 | return &file_chatterbox_proto_enumTypes[0] 65 | } 66 | 67 | func (x What) Number() protoreflect.EnumNumber { 68 | return protoreflect.EnumNumber(x) 69 | } 70 | 71 | // Deprecated: Use What.Descriptor instead. 72 | func (What) EnumDescriptor() ([]byte, []int) { 73 | return file_chatterbox_proto_rawDescGZIP(), []int{0} 74 | } 75 | 76 | type Send struct { 77 | state protoimpl.MessageState 78 | sizeCache protoimpl.SizeCache 79 | unknownFields protoimpl.UnknownFields 80 | 81 | Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` 82 | } 83 | 84 | func (x *Send) Reset() { 85 | *x = Send{} 86 | if protoimpl.UnsafeEnabled { 87 | mi := &file_chatterbox_proto_msgTypes[0] 88 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 89 | ms.StoreMessageInfo(mi) 90 | } 91 | } 92 | 93 | func (x *Send) String() string { 94 | return protoimpl.X.MessageStringOf(x) 95 | } 96 | 97 | func (*Send) ProtoMessage() {} 98 | 99 | func (x *Send) ProtoReflect() protoreflect.Message { 100 | mi := &file_chatterbox_proto_msgTypes[0] 101 | if protoimpl.UnsafeEnabled && x != nil { 102 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 103 | if ms.LoadMessageInfo() == nil { 104 | ms.StoreMessageInfo(mi) 105 | } 106 | return ms 107 | } 108 | return mi.MessageOf(x) 109 | } 110 | 111 | // Deprecated: Use Send.ProtoReflect.Descriptor instead. 112 | func (*Send) Descriptor() ([]byte, []int) { 113 | return file_chatterbox_proto_rawDescGZIP(), []int{0} 114 | } 115 | 116 | func (x *Send) GetText() string { 117 | if x != nil { 118 | return x.Text 119 | } 120 | return "" 121 | } 122 | 123 | type Event struct { 124 | state protoimpl.MessageState 125 | sizeCache protoimpl.SizeCache 126 | unknownFields protoimpl.UnknownFields 127 | 128 | Who string `protobuf:"bytes,1,opt,name=who,proto3" json:"who,omitempty"` 129 | What What `protobuf:"varint,2,opt,name=what,proto3,enum=chatterbox.What" json:"what,omitempty"` 130 | Text string `protobuf:"bytes,3,opt,name=text,proto3" json:"text,omitempty"` 131 | } 132 | 133 | func (x *Event) Reset() { 134 | *x = Event{} 135 | if protoimpl.UnsafeEnabled { 136 | mi := &file_chatterbox_proto_msgTypes[1] 137 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 138 | ms.StoreMessageInfo(mi) 139 | } 140 | } 141 | 142 | func (x *Event) String() string { 143 | return protoimpl.X.MessageStringOf(x) 144 | } 145 | 146 | func (*Event) ProtoMessage() {} 147 | 148 | func (x *Event) ProtoReflect() protoreflect.Message { 149 | mi := &file_chatterbox_proto_msgTypes[1] 150 | if protoimpl.UnsafeEnabled && x != nil { 151 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 152 | if ms.LoadMessageInfo() == nil { 153 | ms.StoreMessageInfo(mi) 154 | } 155 | return ms 156 | } 157 | return mi.MessageOf(x) 158 | } 159 | 160 | // Deprecated: Use Event.ProtoReflect.Descriptor instead. 161 | func (*Event) Descriptor() ([]byte, []int) { 162 | return file_chatterbox_proto_rawDescGZIP(), []int{1} 163 | } 164 | 165 | func (x *Event) GetWho() string { 166 | if x != nil { 167 | return x.Who 168 | } 169 | return "" 170 | } 171 | 172 | func (x *Event) GetWhat() What { 173 | if x != nil { 174 | return x.What 175 | } 176 | return What_INITIALIZED 177 | } 178 | 179 | func (x *Event) GetText() string { 180 | if x != nil { 181 | return x.Text 182 | } 183 | return "" 184 | } 185 | 186 | var File_chatterbox_proto protoreflect.FileDescriptor 187 | 188 | var file_chatterbox_proto_rawDesc = []byte{ 189 | 0x0a, 0x10, 0x63, 0x68, 0x61, 0x74, 0x74, 0x65, 0x72, 0x62, 0x6f, 0x78, 0x2e, 0x70, 0x72, 0x6f, 190 | 0x74, 0x6f, 0x12, 0x0a, 0x63, 0x68, 0x61, 0x74, 0x74, 0x65, 0x72, 0x62, 0x6f, 0x78, 0x1a, 0x1b, 191 | 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 192 | 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x1a, 0x0a, 0x04, 0x53, 193 | 0x65, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 194 | 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x22, 0x53, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 195 | 0x12, 0x10, 0x0a, 0x03, 0x77, 0x68, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x77, 196 | 0x68, 0x6f, 0x12, 0x24, 0x0a, 0x04, 0x77, 0x68, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 197 | 0x32, 0x10, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x74, 0x65, 0x72, 0x62, 0x6f, 0x78, 0x2e, 0x57, 0x68, 198 | 0x61, 0x74, 0x52, 0x04, 0x77, 0x68, 0x61, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 199 | 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x2a, 0x36, 0x0a, 0x04, 200 | 0x57, 0x68, 0x61, 0x74, 0x12, 0x0f, 0x0a, 0x0b, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 201 | 0x5a, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x43, 0x48, 0x41, 0x54, 0x10, 0x01, 0x12, 202 | 0x08, 0x0a, 0x04, 0x4a, 0x4f, 0x49, 0x4e, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x45, 0x41, 203 | 0x56, 0x45, 0x10, 0x03, 0x32, 0x79, 0x0a, 0x0a, 0x43, 0x68, 0x61, 0x74, 0x74, 0x65, 0x72, 0x42, 204 | 0x6f, 0x78, 0x12, 0x31, 0x0a, 0x04, 0x43, 0x68, 0x61, 0x74, 0x12, 0x10, 0x2e, 0x63, 0x68, 0x61, 205 | 0x74, 0x74, 0x65, 0x72, 0x62, 0x6f, 0x78, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x1a, 0x11, 0x2e, 0x63, 206 | 0x68, 0x61, 0x74, 0x74, 0x65, 0x72, 0x62, 0x6f, 0x78, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 207 | 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x38, 0x0a, 0x07, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 208 | 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 209 | 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x74, 210 | 0x65, 0x72, 0x62, 0x6f, 0x78, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x42, 211 | 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x75, 212 | 0x6c, 0x6c, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x64, 0x65, 0x76, 0x2f, 0x67, 0x6f, 0x2f, 0x65, 0x78, 213 | 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x63, 0x68, 0x61, 0x74, 0x74, 0x65, 0x72, 0x62, 0x6f, 214 | 0x78, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 215 | } 216 | 217 | var ( 218 | file_chatterbox_proto_rawDescOnce sync.Once 219 | file_chatterbox_proto_rawDescData = file_chatterbox_proto_rawDesc 220 | ) 221 | 222 | func file_chatterbox_proto_rawDescGZIP() []byte { 223 | file_chatterbox_proto_rawDescOnce.Do(func() { 224 | file_chatterbox_proto_rawDescData = protoimpl.X.CompressGZIP(file_chatterbox_proto_rawDescData) 225 | }) 226 | return file_chatterbox_proto_rawDescData 227 | } 228 | 229 | var file_chatterbox_proto_enumTypes = make([]protoimpl.EnumInfo, 1) 230 | var file_chatterbox_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 231 | var file_chatterbox_proto_goTypes = []interface{}{ 232 | (What)(0), // 0: chatterbox.What 233 | (*Send)(nil), // 1: chatterbox.Send 234 | (*Event)(nil), // 2: chatterbox.Event 235 | (*emptypb.Empty)(nil), // 3: google.protobuf.Empty 236 | } 237 | var file_chatterbox_proto_depIdxs = []int32{ 238 | 0, // 0: chatterbox.Event.what:type_name -> chatterbox.What 239 | 1, // 1: chatterbox.ChatterBox.Chat:input_type -> chatterbox.Send 240 | 3, // 2: chatterbox.ChatterBox.Monitor:input_type -> google.protobuf.Empty 241 | 2, // 3: chatterbox.ChatterBox.Chat:output_type -> chatterbox.Event 242 | 2, // 4: chatterbox.ChatterBox.Monitor:output_type -> chatterbox.Event 243 | 3, // [3:5] is the sub-list for method output_type 244 | 1, // [1:3] is the sub-list for method input_type 245 | 1, // [1:1] is the sub-list for extension type_name 246 | 1, // [1:1] is the sub-list for extension extendee 247 | 0, // [0:1] is the sub-list for field type_name 248 | } 249 | 250 | func init() { file_chatterbox_proto_init() } 251 | func file_chatterbox_proto_init() { 252 | if File_chatterbox_proto != nil { 253 | return 254 | } 255 | if !protoimpl.UnsafeEnabled { 256 | file_chatterbox_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 257 | switch v := v.(*Send); i { 258 | case 0: 259 | return &v.state 260 | case 1: 261 | return &v.sizeCache 262 | case 2: 263 | return &v.unknownFields 264 | default: 265 | return nil 266 | } 267 | } 268 | file_chatterbox_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 269 | switch v := v.(*Event); i { 270 | case 0: 271 | return &v.state 272 | case 1: 273 | return &v.sizeCache 274 | case 2: 275 | return &v.unknownFields 276 | default: 277 | return nil 278 | } 279 | } 280 | } 281 | type x struct{} 282 | out := protoimpl.TypeBuilder{ 283 | File: protoimpl.DescBuilder{ 284 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 285 | RawDescriptor: file_chatterbox_proto_rawDesc, 286 | NumEnums: 1, 287 | NumMessages: 2, 288 | NumExtensions: 0, 289 | NumServices: 1, 290 | }, 291 | GoTypes: file_chatterbox_proto_goTypes, 292 | DependencyIndexes: file_chatterbox_proto_depIdxs, 293 | EnumInfos: file_chatterbox_proto_enumTypes, 294 | MessageInfos: file_chatterbox_proto_msgTypes, 295 | }.Build() 296 | File_chatterbox_proto = out.File 297 | file_chatterbox_proto_rawDesc = nil 298 | file_chatterbox_proto_goTypes = nil 299 | file_chatterbox_proto_depIdxs = nil 300 | } 301 | --------------------------------------------------------------------------------