├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── workflows │ └── go.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── backends └── mem │ ├── example_test.go │ ├── server.go │ ├── server_test.go │ ├── topic.go │ └── topic_test.go ├── decorators ├── base64 │ ├── receiver.go │ ├── receiver_test.go │ ├── topic.go │ └── topic_test.go ├── lz4 │ ├── receiver.go │ ├── receiver_test.go │ ├── topic.go │ └── topic_test.go ├── otel │ └── tracing │ │ ├── doc.go │ │ ├── receiver.go │ │ ├── receiver_test.go │ │ ├── topic.go │ │ ├── topic_test.go │ │ ├── tracing_test.go │ │ └── utils.go └── tracing │ ├── doc.go │ ├── receiver.go │ ├── receiver_test.go │ ├── topic.go │ ├── topic_test.go │ └── utils.go ├── docs └── decisions │ ├── 2024-07-08-multiserver.md │ └── 2024-07-09-experimental-changes.md ├── go.mod ├── go.sum ├── msg.go ├── msg_test.go └── x ├── README.md └── multiserver ├── README.md ├── multiserver.go ├── multiserver_test.go └── receiver.go /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome all contributions to this project! 4 | 5 | ## Submitting a Pull Request 6 | 7 | * Fork and clone the repo 8 | * Create a new branch: `git checkout -b my-branch` 9 | * Make your change, add any tests (make sure they pass!) 10 | * Push to your fork and submit a pull request 11 | 12 | ## Best Practices 13 | 14 | * Follow the [Effective Go](https://golang.org/doc/effective_go.html#formatting) style guide 15 | (tl;dr just use gofmt). 16 | * Write good commit messages 17 | * Write tests where possible 18 | * Use the Makefile to your advantage 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > Please start by opening a Github issue to propose a fix or enhancement to the project. 2 | This will allow the community to provide feedback and initiate a discussion re: the new issue. 3 | 4 | > If this is a bug, please provide a test case and any sample code and/or output. 5 | 6 | > Please label your issue accordingly using the labels on the right 7 | 8 | > Please be patient if your issue is not resolved as quickly as you would expect. We all get busy sometimes :P 9 | 10 | > Thank you for contributing!! 11 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: go-msg 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | lint: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | include: 19 | - go-version: 1.20.x 20 | lint-version: v1.53.3 21 | - go-version: 1.21.x 22 | lint-version: v1.55.2 23 | - go-version: 1.22.x 24 | lint-version: v1.58.1 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-go@v5 28 | with: 29 | go-version: ${{ matrix.go-version }} 30 | - name: golangci-lint 31 | uses: golangci/golangci-lint-action@v6 32 | with: 33 | args: -v 34 | version: ${{ matrix.lint-version }} 35 | 36 | tests: 37 | name: tests 38 | runs-on: ubuntu-latest 39 | strategy: 40 | matrix: 41 | include: 42 | - go-version: 1.20.x 43 | - go-version: 1.21.x 44 | - go-version: 1.22.x 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-go@v5 48 | name: "install go" 49 | with: 50 | go-version: ${{ matrix.go-version }} 51 | - name: "tests" 52 | run: go test ./... 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | presets: 4 | - format -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Clear BSD License 2 | 3 | Copyright (c) 2017, ZeroFOX 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted (subject to the limitations in the disclaimer 8 | below) provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its contributors may be used 18 | to endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS 22 | LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 24 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 25 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 26 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 27 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 28 | GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 29 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 31 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 32 | DAMAGE. 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGES=$(shell go list ./...) 2 | 3 | all: lint test 4 | 5 | init: tools 6 | GO111MODULE=on go mod vendor 7 | 8 | lint: init 9 | golangci-lint run ./... 10 | 11 | test: init 12 | go test -race ./... 13 | 14 | tools: 15 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.17.0 16 | 17 | fmt: tools 18 | go fmt $(PACKAGES) 19 | 20 | .PHONY: help lint test fmt tools 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-msg 2 | 3 | [![Build Status](https://github.com/zerofox-oss/go-msg/actions/workflows/go.yml/badge.svg)](https://github.com/zerofox-oss/go-msg/actions/workflows/go.yml) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/zerofox-oss/go-msg.svg)](https://pkg.go.dev/github.com/zerofox-oss/go-msg) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/zerofox-oss/go-msg)](https://goreportcard.com/report/github.com/zerofox-oss/go-msg) 6 | [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/zerofox-oss/go-msg/badge)](https://scorecard.dev/viewer/?uri=github.com/zerofox-oss/go-msg) 7 | 8 | **Pub/Sub Message Primitives for Go** 9 | 10 | This library contains 11 | the basic primitives 12 | for developing [pub-sub][] systems. 13 | 14 | *Messages* are published to *Topics*. 15 | *Servers* subscribe to *Messages*. 16 | 17 | These primitives specify 18 | abstract behavior of pub-sub; 19 | they do not specify implementation. 20 | A *Message* could exist in 21 | an in-memory array, 22 | a file, 23 | a key/value store like RabbitMQ, 24 | or even something like 25 | Amazon SNS/SQS 26 | or Google Pub/Sub. 27 | In order to tap into 28 | that backend, 29 | a concrete implementation 30 | must be written for it. 31 | 32 | Here's a list of backends that are currently supported: 33 | 34 | | Backend | Link | 35 | | :------------- | :------------- | 36 | | Channels | https://github.com/zerofox-oss/go-msg/backends/mem | 37 | | AWS (SNS,SQS) | https://github.com/zerofox-oss/go-aws-msg | 38 | | Google PubSub | https://github.com/paultyng/go-msg-pubsub | 39 | 40 | ## How it works 41 | 42 | ### Backend 43 | 44 | A backend simply represents 45 | the infrastructure behind a pub-sub system. 46 | This is where *Messages* live. 47 | 48 | Examples could include a key/value store, 49 | Google Pub/Sub, or Amazon SNS + SQS. 50 | 51 | ### Message 52 | 53 | A *Message* represents a discrete unit of data. 54 | It contains a body 55 | and a list of attributes. 56 | *Attributes* can be used to distinguish 57 | unique properties of a *Message*, 58 | including how to read the body. 59 | More on that in [decorator patterns][]. 60 | 61 | ### Publish 62 | 63 | A *Message* is published to a *Topic*. 64 | A *Topic* writes the body 65 | and attributes of a *Message* 66 | to a backend using a *MessageWriter*. 67 | A *MessageWriter* may only 68 | be used for one *Message*, 69 | much like a [net/http ResponseWriter][http_responsewriter] 70 | 71 | When the *MessageWriter* is closed, 72 | the data that was written to it 73 | will be published to that backend 74 | and it will no longer be able to be used. 75 | 76 | ### Subscribe 77 | 78 | A *Server* subscribes to *Messages* 79 | from a backend. 80 | It's important to note that 81 | a *Server* must know how to convert 82 | raw data to a *Message* - 83 | this will be unique to each backend. 84 | For example, the way you read 85 | message attributes from a file 86 | is very different from 87 | how you read them from SQS. 88 | A *Server* is always live, 89 | so it will continue to 90 | block indefinitely while 91 | it is waiting for messages 92 | until it is shut down. 93 | 94 | When a *Message* is created, 95 | the *Server* passes it to 96 | a *Receiver* for processing. 97 | This is similar to how 98 | [net/http Handler][http_handler] works. 99 | A *Receiver* may return an error 100 | if it was unable to process the *Message*. 101 | This will indicate to the *Server* 102 | that the *Message* must be retried. 103 | The specifics to this retry logic 104 | will be specific to each backend. 105 | 106 | ## Benefits 107 | 108 | This library was originally conceived 109 | because we needed a way 110 | to reduce copy-pasted code 111 | across our pub-sub systems 112 | and we wanted to try out other infrastructures. 113 | 114 | These primitives allow us to 115 | achieve both of those goals. 116 | Want to try out Kafka instead of AWS? 117 | No problem! 118 | Just write a library 119 | that utilizes these primitives 120 | and the Kafka SDK. 121 | 122 | What these primitives 123 | or any implementation 124 | of these primitives **DO NOT DO** 125 | is mask or replace all of the functionality 126 | of all infrastructures. 127 | If you want to use 128 | a particular feature of AWS 129 | that does not fit 130 | with these primitives, 131 | that's OK. 132 | It might make sense 133 | to add that feature 134 | to the primitives, 135 | it might not. 136 | We encourage you to open 137 | an issue to discuss such additions. 138 | 139 | Aside from the code re-use benefits, 140 | there's a number of other features 141 | which we believe are useful, including: 142 | 143 | * Concrete implementations can be written once 144 | and distributed as libraries. 145 | 146 | * Decorator Patterns 147 | 148 | * Built-in concurrency controls into *Server*. 149 | 150 | * Context deadlines and cancellations. 151 | This allows for clean shutdowns to prevent data loss. 152 | 153 | * Transaction-based *Receivers*. 154 | 155 | [pub-sub]: https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern 156 | [http_handler]: https://golang.org/pkg/net/http/#HandlerFunc.ServeHTTP 157 | [http_responsewriter]: https://golang.org/pkg/net/http/#ResponseWriter -------------------------------------------------------------------------------- /backends/mem/example_test.go: -------------------------------------------------------------------------------- 1 | package mem_test 2 | 3 | import ( 4 | "context" 5 | "encoding/csv" 6 | "fmt" 7 | "sort" 8 | 9 | "github.com/zerofox-oss/go-msg" 10 | "github.com/zerofox-oss/go-msg/backends/mem" 11 | ) 12 | 13 | func Example_primitives() { 14 | c1 := make(chan *msg.Message, 10) 15 | c2 := make(chan *msg.Message, 10) 16 | c3 := make(chan *msg.Message, 10) 17 | 18 | t1, t2, t3 := mem.Topic{C: c1}, mem.Topic{C: c2}, mem.Topic{C: c3} 19 | srv1, srv2 := mem.NewServer(c1, 1), mem.NewServer(c2, 1) 20 | 21 | // split csv into separate messages for analysis 22 | go func() { 23 | splitFunc := msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { 24 | lines, err := csv.NewReader(m.Body).ReadAll() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | for _, row := range lines { 30 | for _, col := range row { 31 | w := t2.NewWriter(ctx) 32 | w.Write([]byte(col)) 33 | w.Close() 34 | } 35 | } 36 | return nil 37 | }) 38 | srv1.Serve(splitFunc) 39 | }() 40 | 41 | // perform some analysis on each message 42 | go func() { 43 | analyzeFunc := msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { 44 | body, err := msg.DumpBody(m) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | w := t3.NewWriter(ctx) 50 | w.Attributes().Set("Length", fmt.Sprintf("%d", len(body))) 51 | w.Attributes().Set("StartsWith", string(body[0:1])) 52 | 53 | if len(body)%2 == 0 { 54 | w.Attributes().Set("Even", "true") 55 | w.Attributes().Set("Odd", "false") 56 | } else { 57 | w.Attributes().Set("Even", "false") 58 | w.Attributes().Set("Odd", "true") 59 | } 60 | w.Write(body) 61 | return w.Close() 62 | }) 63 | srv2.Serve(analyzeFunc) 64 | }() 65 | 66 | messages := [][]byte{ 67 | []byte("foo,bar,baz"), 68 | []byte("one,two,three,four"), 69 | } 70 | 71 | for _, m := range messages { 72 | w := t1.NewWriter(context.Background()) 73 | w.Write(m) 74 | w.Close() 75 | } 76 | 77 | calls, expectedCalls := 0, 7 78 | 79 | for m := range c3 { 80 | orderedKeys := make([]string, 0) 81 | for k := range m.Attributes { 82 | orderedKeys = append(orderedKeys, k) 83 | } 84 | sort.Strings(orderedKeys) 85 | 86 | for _, k := range orderedKeys { 87 | fmt.Printf("%s:%v ", k, m.Attributes.Get(k)) 88 | } 89 | fmt.Printf("%v\n", m.Body) 90 | 91 | calls++ 92 | if calls == expectedCalls { 93 | close(c3) 94 | } 95 | } 96 | 97 | // Output: 98 | // Even:false Length:3 Odd:true Startswith:f foo 99 | // Even:false Length:3 Odd:true Startswith:b bar 100 | // Even:false Length:3 Odd:true Startswith:b baz 101 | // Even:false Length:3 Odd:true Startswith:o one 102 | // Even:false Length:3 Odd:true Startswith:t two 103 | // Even:false Length:5 Odd:true Startswith:t three 104 | // Even:true Length:4 Odd:false Startswith:f four 105 | } 106 | -------------------------------------------------------------------------------- /backends/mem/server.go: -------------------------------------------------------------------------------- 1 | package mem 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/zerofox-oss/go-msg" 9 | ) 10 | 11 | // Server subscribes to a channel and listens for Messages. 12 | type Server struct { 13 | C chan *msg.Message 14 | 15 | // Concurrency is the maximum number of Messages that can be processed 16 | // concurrently by the Server. 17 | Concurrency int 18 | 19 | // maxConcurrentReceives is a buffered channel which acts as 20 | // a shared lock that limits the number of concurrent goroutines 21 | maxConcurrentReceives chan struct{} 22 | 23 | listenerCtx context.Context 24 | listenerCancelFunc context.CancelFunc 25 | 26 | receiverCtx context.Context 27 | receiverCancelFunc context.CancelFunc 28 | } 29 | 30 | // Ensure that Server implements msg.Server 31 | var _ msg.Server = &Server{} 32 | 33 | // Serve always returns a non-nil error. 34 | // After Shutdown, the returned error is ErrServerClosed 35 | func (s *Server) Serve(r msg.Receiver) error { 36 | for { 37 | select { 38 | case <-s.listenerCtx.Done(): 39 | close(s.maxConcurrentReceives) 40 | return msg.ErrServerClosed 41 | 42 | case m := <-s.C: 43 | if m == nil { 44 | continue 45 | } 46 | 47 | // acquire "lock" 48 | s.maxConcurrentReceives <- struct{}{} 49 | 50 | go func(ctx context.Context, m *msg.Message) { 51 | defer func() { 52 | <-s.maxConcurrentReceives 53 | }() 54 | 55 | if err := r.Receive(ctx, m); err != nil { 56 | log.Printf("could not receive message %s", err) 57 | s.C <- m 58 | } 59 | }(s.receiverCtx, m) 60 | } 61 | } 62 | } 63 | 64 | // shutdownPollInterval is how often we poll for quiescence 65 | // during Server.Shutdown. 66 | const shutdownPollInterval = 50 * time.Millisecond 67 | 68 | // Shutdown attempts to gracefully shut down the Server without 69 | // interrupting any messages in flight. 70 | // When Shutdown is signalled, the Server stops polling for new Messages 71 | // and then it waits for all of the active goroutines to complete. 72 | // 73 | // If the provided context expires before the shutdown is complete, 74 | // then any remaining goroutines will be killed and the context's error 75 | // is returned. 76 | func (s *Server) Shutdown(ctx context.Context) error { 77 | if ctx == nil { 78 | panic("invalid context (nil)") 79 | } 80 | s.listenerCancelFunc() 81 | 82 | ticker := time.NewTicker(shutdownPollInterval) 83 | defer ticker.Stop() 84 | 85 | for { 86 | select { 87 | case <-ctx.Done(): 88 | s.receiverCancelFunc() 89 | return ctx.Err() 90 | 91 | case <-ticker.C: 92 | if len(s.maxConcurrentReceives) == 0 { 93 | return msg.ErrServerClosed 94 | } 95 | } 96 | } 97 | } 98 | 99 | // NewServer creates and initializes a new Server. 100 | func NewServer(c chan *msg.Message, cc int) *Server { 101 | listenerCtx, listenerCancelFunc := context.WithCancel(context.Background()) 102 | receiverCtx, receiverCancelFunc := context.WithCancel(context.Background()) 103 | 104 | srv := &Server{ 105 | C: c, 106 | Concurrency: cc, 107 | 108 | listenerCtx: listenerCtx, 109 | listenerCancelFunc: listenerCancelFunc, 110 | receiverCtx: receiverCtx, 111 | receiverCancelFunc: receiverCancelFunc, 112 | maxConcurrentReceives: make(chan struct{}, cc), 113 | } 114 | return srv 115 | } 116 | -------------------------------------------------------------------------------- /backends/mem/server_test.go: -------------------------------------------------------------------------------- 1 | package mem_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "testing" 9 | "time" 10 | 11 | "github.com/zerofox-oss/go-msg" 12 | "github.com/zerofox-oss/go-msg/backends/mem" 13 | ) 14 | 15 | // ConcurrentReceiver writes to an channel upon consumption of a Message. 16 | // It is safe to utilize by concurrent goroutines. 17 | type ConcurrentReceiver struct { 18 | t *testing.T 19 | C chan struct{} 20 | } 21 | 22 | func (r *ConcurrentReceiver) Receive(ctx context.Context, m *msg.Message) error { 23 | select { 24 | case <-ctx.Done(): 25 | return ctx.Err() 26 | default: 27 | r.C <- struct{}{} 28 | } 29 | return nil 30 | } 31 | 32 | // RetryReceiver returns an error upon consumption of a Message. Once it 33 | // has been called a certain number of times, it writes to an channel and 34 | // returns nil. 35 | // 36 | // It it safe to utilize by concurrent goroutines. 37 | type RetryReceiver struct { 38 | t *testing.T 39 | C chan struct{} 40 | 41 | allowedRetries int 42 | calls int 43 | } 44 | 45 | func (r *RetryReceiver) Receive(ctx context.Context, m *msg.Message) error { 46 | select { 47 | case <-ctx.Done(): 48 | r.t.Fatalf("[ERROR] Receiver could not finish executing") 49 | default: 50 | } 51 | 52 | if r.calls == r.allowedRetries { 53 | r.C <- struct{}{} 54 | return nil 55 | } 56 | r.calls++ 57 | 58 | return errors.New("could not complete transaction") 59 | } 60 | 61 | // TestServer_Serve asserts that the Server can process all messages that 62 | // have been sent to its input buffer using a single goroutine 63 | func TestServer_Serve(t *testing.T) { 64 | messages := []*msg.Message{ 65 | { 66 | Attributes: msg.Attributes{}, 67 | Body: bytes.NewBufferString("message #1: hello world!"), 68 | }, 69 | { 70 | Attributes: msg.Attributes{}, 71 | Body: bytes.NewBufferString("message #2: foo bar"), 72 | }, 73 | { 74 | Attributes: msg.Attributes{}, 75 | Body: bytes.NewBufferString("message #3: gophercon9000"), 76 | }, 77 | } 78 | 79 | srv := mem.NewServer(make(chan *msg.Message, len(messages)), 1) 80 | 81 | for _, m := range messages { 82 | srv.C <- m 83 | } 84 | defer close(srv.C) 85 | 86 | outputChannel := make(chan struct{}) 87 | go func() { 88 | srv.Serve(&ConcurrentReceiver{ 89 | t: t, 90 | C: outputChannel, 91 | }) 92 | }() 93 | 94 | // block until all requests have been made 95 | calls := 0 96 | for range outputChannel { 97 | calls++ 98 | if calls == len(messages) { 99 | close(outputChannel) 100 | } 101 | } 102 | } 103 | 104 | // TestServer_Serve asserts that the Server can process all messages that 105 | // have been sent to its input buffer using lots of concurrent goroutines 106 | func TestServer_ServeConcurrency(t *testing.T) { 107 | var messages [10000]*msg.Message 108 | 109 | for i := 0; i < len(messages); i++ { 110 | messages[i] = &msg.Message{ 111 | Attributes: msg.Attributes{}, 112 | Body: bytes.NewBufferString(fmt.Sprintf("this is a test message #%d", i)), 113 | } 114 | } 115 | 116 | srv := mem.NewServer(make(chan *msg.Message, len(messages)), 10) 117 | 118 | for _, m := range messages { 119 | srv.C <- m 120 | } 121 | defer close(srv.C) 122 | 123 | outputChannel := make(chan struct{}) 124 | go func() { 125 | srv.Serve(&ConcurrentReceiver{ 126 | t: t, 127 | C: outputChannel, 128 | }) 129 | }() 130 | 131 | // block until all requests have been made 132 | calls := 0 133 | for range outputChannel { 134 | calls++ 135 | if calls == len(messages) { 136 | close(outputChannel) 137 | } 138 | } 139 | } 140 | 141 | // TestServer_ServeCanRetryMessages asserts that the Server can handle 142 | // errors returned by the Receiver and "retry" the message that caused 143 | // the error 144 | func TestServer_ServeCanRetryMessages(t *testing.T) { 145 | messages := []*msg.Message{ 146 | { 147 | Attributes: msg.Attributes{}, 148 | Body: bytes.NewBufferString("message #1: hello world!"), 149 | }, 150 | } 151 | 152 | srv := mem.NewServer(make(chan *msg.Message, len(messages)), 1) 153 | 154 | for _, m := range messages { 155 | srv.C <- m 156 | } 157 | defer close(srv.C) 158 | 159 | outputChannel := make(chan struct{}) 160 | go func() { 161 | srv.Serve(&RetryReceiver{ 162 | t: t, 163 | C: outputChannel, 164 | 165 | calls: 0, 166 | allowedRetries: 10, 167 | }) 168 | }() 169 | 170 | // after 10th retry receiver will write to channel 171 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 172 | defer cancel() 173 | 174 | select { 175 | case <-ctx.Done(): 176 | t.Fatalf("context timed out") 177 | case <-outputChannel: 178 | close(outputChannel) 179 | } 180 | } 181 | 182 | // TestServer_ShutdownContextTimeoutExceeded tests that the Server can 183 | // use a Context to shut down before all of it's goroutines have finished. 184 | func TestServer_ShutdownContextTimeoutExceeded(t *testing.T) { 185 | done := make(chan struct{}) 186 | 187 | message := &msg.Message{ 188 | Attributes: msg.Attributes{}, 189 | Body: bytes.NewBufferString("hello world!"), 190 | } 191 | 192 | srv := mem.NewServer(make(chan *msg.Message, 1), 1) 193 | defer close(srv.C) 194 | 195 | receiver := msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { 196 | defer close(done) 197 | 198 | // set a fast timeout so srv doesn't have enough time to finish 199 | sctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 200 | defer cancel() 201 | 202 | if err := srv.Shutdown(sctx); err == msg.ErrServerClosed { 203 | t.Fatal("expected context timeout") 204 | } 205 | return nil 206 | }) 207 | srv.C <- message 208 | 209 | if err := srv.Serve(receiver); err != msg.ErrServerClosed { 210 | t.Fatalf("expected %v, got %v", msg.ErrServerClosed, err) 211 | } 212 | 213 | <-done 214 | } 215 | -------------------------------------------------------------------------------- /backends/mem/topic.go: -------------------------------------------------------------------------------- 1 | package mem 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "sync" 7 | "time" 8 | 9 | "github.com/zerofox-oss/go-msg" 10 | ) 11 | 12 | // Topic publishes Messages to a channel. 13 | type Topic struct { 14 | C chan *msg.Message 15 | } 16 | 17 | // Ensure that Topic implements msg.Topic 18 | var _ msg.Topic = &Topic{} 19 | 20 | // NewWriter returns a MessageWriter. 21 | // The MessageWriter may be used to write messages to a channel. 22 | func (t *Topic) NewWriter(context.Context) msg.MessageWriter { 23 | return &MessageWriter{ 24 | c: t.C, 25 | 26 | attributes: make(map[string][]string), 27 | buf: &bytes.Buffer{}, 28 | } 29 | } 30 | 31 | // MessageWriter is used to publish a single Message to a channel. 32 | // Once all of the data has been written and closed, it may not be used again. 33 | type MessageWriter struct { 34 | msg.MessageWriter 35 | 36 | c chan *msg.Message 37 | 38 | attributes msg.Attributes 39 | buf *bytes.Buffer // internal buffer 40 | closed bool 41 | mux sync.Mutex 42 | } 43 | 44 | // Attributes returns the attributes of the MessageWriter. 45 | func (w *MessageWriter) Attributes() *msg.Attributes { 46 | return &w.attributes 47 | } 48 | 49 | func (w *MessageWriter) SetDelay(_ time.Duration) {} 50 | 51 | // Close publishes a Message to a channel. 52 | // If the MessageWriter is already closed it will return an error. 53 | func (w *MessageWriter) Close() error { 54 | w.mux.Lock() 55 | defer w.mux.Unlock() 56 | 57 | if w.closed { 58 | return msg.ErrClosedMessageWriter 59 | } 60 | w.closed = true 61 | 62 | if w.buf.Len() > 0 { 63 | msg := &msg.Message{ 64 | Attributes: w.attributes, 65 | Body: w.buf, 66 | } 67 | w.c <- msg 68 | } 69 | 70 | return nil 71 | } 72 | 73 | // Write writes bytes to an internal buffer. 74 | func (w *MessageWriter) Write(p []byte) (int, error) { 75 | w.mux.Lock() 76 | defer w.mux.Unlock() 77 | 78 | if w.closed { 79 | return 0, msg.ErrClosedMessageWriter 80 | } 81 | return w.buf.Write(p) 82 | } 83 | -------------------------------------------------------------------------------- /backends/mem/topic_test.go: -------------------------------------------------------------------------------- 1 | package mem_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/zerofox-oss/go-msg" 8 | "github.com/zerofox-oss/go-msg/backends/mem" 9 | ) 10 | 11 | func TestMessageWriter_Attributes(t *testing.T) { 12 | testTopic := &mem.Topic{} 13 | 14 | w := testTopic.NewWriter(context.Background()) 15 | attrs := w.Attributes() 16 | attrs.Set("test", "value") 17 | 18 | if attrs.Get("test") != "value" { 19 | t.Errorf("expected attribute to be value, got %v", attrs.Get("test")) 20 | } 21 | } 22 | 23 | func TestMessageWriter_WriteAndClose(t *testing.T) { 24 | channel := make(chan *msg.Message) 25 | testTopic := &mem.Topic{ 26 | C: channel, 27 | } 28 | 29 | go func() { 30 | w := testTopic.NewWriter(context.Background()) 31 | w.Write([]byte("Don't ")) 32 | w.Write([]byte("call me ")) 33 | w.Write([]byte("junior!")) 34 | w.Close() 35 | }() 36 | 37 | m := <-channel 38 | 39 | body, err := msg.DumpBody(m) 40 | if err != nil { 41 | t.Fatalf("could not read bytes from body: %s", err.Error()) 42 | } 43 | 44 | expected := []byte("Don't call me junior!") 45 | if string(body) != string(expected) { 46 | t.Errorf("expected %s got %s", string(expected), string(body)) 47 | } 48 | } 49 | 50 | // asserts MessageWriter can only be used once 51 | func TestMesageWriter_SingleUse(t *testing.T) { 52 | channel := make(chan *msg.Message) 53 | testTopic := &mem.Topic{ 54 | C: channel, 55 | } 56 | 57 | w := testTopic.NewWriter(context.Background()) 58 | 59 | text := [][]byte{ 60 | []byte("I have a bad feeling about this..."), 61 | []byte("Great shot kid that was one in a million!"), 62 | } 63 | 64 | go func() { 65 | if _, err := w.Write(text[0]); err != nil { 66 | t.Error(err) 67 | } 68 | if err := w.Close(); err != nil { 69 | t.Error(err) 70 | } 71 | 72 | // second write, close should fail 73 | if _, err := w.Write(text[1]); err != msg.ErrClosedMessageWriter { 74 | t.Errorf("expected %v, got %v", msg.ErrClosedMessageWriter, err) 75 | } 76 | if err := w.Close(); err != msg.ErrClosedMessageWriter { 77 | t.Errorf("expected %v, got %v", msg.ErrClosedMessageWriter, err) 78 | } 79 | }() 80 | 81 | m := <-channel 82 | 83 | body, err := msg.DumpBody(m) 84 | if err != nil { 85 | t.Fatalf("could not read bytes from body: %s", err.Error()) 86 | } 87 | 88 | if string(body) != string(text[0]) { 89 | t.Errorf("expected %s got %s", string(text[0]), string(body)) 90 | } 91 | } 92 | 93 | // asserts MessageWriter does not emit an empty message on Close if Write was never called. 94 | func TestMesageWriter_CloseEmpty(t *testing.T) { 95 | channel := make(chan *msg.Message) 96 | testTopic := &mem.Topic{ 97 | C: channel, 98 | } 99 | 100 | w := testTopic.NewWriter(context.Background()) 101 | 102 | if err := w.Close(); err != nil { 103 | t.Error(err) 104 | } 105 | 106 | count := len(channel) 107 | 108 | if count != 0 { 109 | t.Errorf("got %d messages in channel, wanted 0", count) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /decorators/base64/receiver.go: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | 7 | "github.com/zerofox-oss/go-msg" 8 | ) 9 | 10 | // Decoder wraps a msg.Receiver with base64 decoding functionality. 11 | // It only attempts to decode the Message.Body if Content-Transfer-Encoding 12 | // is set to base64. 13 | func Decoder(next msg.Receiver) msg.Receiver { 14 | return msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { 15 | select { 16 | case <-ctx.Done(): 17 | return ctx.Err() 18 | default: 19 | if isBase64Encoded(m) { 20 | m.Body = base64.NewDecoder(base64.StdEncoding, m.Body) 21 | } 22 | 23 | return next.Receive(ctx, m) 24 | } 25 | }) 26 | } 27 | 28 | // isBase64Encoded returns true if Content-Transfer-Encoding is set to 29 | // "base64" in the passed Message's Attributes. 30 | // 31 | // Note: MIMEHeader.Get() is used to fetch this value. In the case of a list of 32 | // values, .Get() returns the 0th value. 33 | func isBase64Encoded(m *msg.Message) bool { 34 | return m.Attributes.Get("Content-Transfer-Encoding") == "base64" 35 | } 36 | -------------------------------------------------------------------------------- /decorators/base64/receiver_test.go: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/zerofox-oss/go-msg" 10 | ) 11 | 12 | type ChanReceiver struct { 13 | c chan *msg.Message 14 | } 15 | 16 | func (r ChanReceiver) Receive(ctx context.Context, m *msg.Message) error { 17 | r.c <- m 18 | return nil 19 | } 20 | 21 | // Tests that when a Receiver is wrapped by Decoder, the body of a message 22 | // is base64 decoded when the Content-Transfer-Encoding header is set to base64. 23 | func TestDecoder_SuccessfullyDecodesWhenHeaderIsSet(t *testing.T) { 24 | testFinish := make(chan struct{}) 25 | msgChan := make(chan *msg.Message) 26 | r := Decoder(ChanReceiver{ 27 | c: msgChan, 28 | }) 29 | 30 | ctx, cancel := context.WithCancel(context.Background()) 31 | defer cancel() 32 | 33 | // Construct a message with base64 encoding (YWJjMTIz == abc123) 34 | m := &msg.Message{ 35 | Body: bytes.NewBufferString("YWJjMTIz"), 36 | Attributes: msg.Attributes{}, 37 | } 38 | m.Attributes.Set("Content-Transfer-Encoding", "base64") 39 | 40 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 41 | go func() { 42 | result := <-msgChan 43 | 44 | expectedBody := "abc123" 45 | actual, _ := ioutil.ReadAll(result.Body) 46 | if string(actual) != expectedBody { 47 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 48 | } 49 | 50 | testFinish <- struct{}{} 51 | }() 52 | 53 | // Receive the message! 54 | err := r.Receive(ctx, m) 55 | if err != nil { 56 | t.Error(err) 57 | return 58 | } 59 | <-testFinish 60 | } 61 | 62 | // Tests that when a Receiver is wrapped by Decoder, the body of a message 63 | // is not changed if Content-Transfer-Encoding is not set to base64. 64 | func TestDecoder_DoesNotModifyMessageWithoutAppropriateHeader(t *testing.T) { 65 | testFinish := make(chan struct{}) 66 | msgChan := make(chan *msg.Message) 67 | r := Decoder(ChanReceiver{ 68 | c: msgChan, 69 | }) 70 | 71 | ctx, cancel := context.WithCancel(context.Background()) 72 | defer cancel() 73 | 74 | // Construct a message without base64 encoding 75 | m := &msg.Message{ 76 | Body: bytes.NewBufferString("abc123"), 77 | Attributes: msg.Attributes{}, 78 | } 79 | 80 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 81 | go func() { 82 | result := <-msgChan 83 | 84 | expectedBody := "abc123" 85 | actual, _ := ioutil.ReadAll(result.Body) 86 | if string(actual) != expectedBody { 87 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 88 | } 89 | 90 | testFinish <- struct{}{} 91 | }() 92 | 93 | // Receive the message! 94 | err := r.Receive(ctx, m) 95 | if err != nil { 96 | t.Error(err) 97 | return 98 | } 99 | <-testFinish 100 | } 101 | 102 | // Tests that isBase64Encoded properly identifies Content-Transfer-Encoding. 103 | func TestIsBase64Encoded_True(t *testing.T) { 104 | m := &msg.Message{ 105 | Attributes: msg.Attributes{}, 106 | } 107 | m.Attributes.Set("Content-Transfer-Encoding", "base64") 108 | 109 | if !isBase64Encoded(m) { 110 | t.Error("Expected m to be base64 encoded but got false.") 111 | } 112 | } 113 | 114 | // Tests that isBase64Encoded returns false if Content-Transfer-Encoding is 115 | // set to a value other than base64. 116 | func TestIsBase64Encoded_FalseOtherValueInContentTransfer(t *testing.T) { 117 | m := &msg.Message{ 118 | Attributes: msg.Attributes{}, 119 | } 120 | m.Attributes.Set("Content-Transfer-Encoding", "base65") 121 | 122 | if isBase64Encoded(m) { 123 | t.Error("Expected m not to be base64 encoded but got true.") 124 | } 125 | } 126 | 127 | // Tests that isBase64Encoded returns false if Content-Transfer-Encoding is 128 | // not set. 129 | func TestIsBase64Encoded_FalseContentTransferNotSet(t *testing.T) { 130 | m := &msg.Message{ 131 | Attributes: msg.Attributes{}, 132 | } 133 | m.Attributes.Set("Other-Header", "base64") 134 | 135 | if isBase64Encoded(m) { 136 | t.Error("Expected m not to be base64 encoded but got true.") 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /decorators/base64/topic.go: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "sync" 8 | "time" 9 | 10 | "github.com/zerofox-oss/go-msg" 11 | ) 12 | 13 | // Encoder wraps a topic with another which base64-encodes a Message. 14 | func Encoder(next msg.Topic) msg.Topic { 15 | return msg.TopicFunc(func(ctx context.Context) msg.MessageWriter { 16 | return &encodeWriter{ 17 | Next: next.NewWriter(ctx), 18 | } 19 | }) 20 | } 21 | 22 | type encodeWriter struct { 23 | Next msg.MessageWriter 24 | 25 | buf bytes.Buffer 26 | closed bool 27 | mux sync.Mutex 28 | } 29 | 30 | // Attributes returns the attributes associated with the MessageWriter. 31 | func (w *encodeWriter) Attributes() *msg.Attributes { 32 | return w.Next.Attributes() 33 | } 34 | 35 | func (w *encodeWriter) SetDelay(delay time.Duration) { 36 | w.Next.SetDelay(delay) 37 | } 38 | 39 | // Close base64-encodes the contents of the buffer before 40 | // writing them to the next MessageWriter. 41 | func (w *encodeWriter) Close() error { 42 | w.mux.Lock() 43 | defer w.mux.Unlock() 44 | 45 | if w.closed { 46 | return msg.ErrClosedMessageWriter 47 | } 48 | w.closed = true 49 | 50 | attrs := *w.Attributes() 51 | attrs["Content-Transfer-Encoding"] = []string{"base64"} 52 | 53 | src := w.buf.Bytes() 54 | buf := make([]byte, base64.StdEncoding.EncodedLen(len(src))) 55 | base64.StdEncoding.Encode(buf, src) 56 | 57 | if _, err := w.Next.Write(buf); err != nil { 58 | return err 59 | } 60 | return w.Next.Close() 61 | } 62 | 63 | // Write writes bytes to an internal buffer. 64 | func (w *encodeWriter) Write(b []byte) (int, error) { 65 | w.mux.Lock() 66 | defer w.mux.Unlock() 67 | 68 | if w.closed { 69 | return 0, msg.ErrClosedMessageWriter 70 | } 71 | return w.buf.Write(b) 72 | } 73 | -------------------------------------------------------------------------------- /decorators/base64/topic_test.go: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | msg "github.com/zerofox-oss/go-msg" 8 | "github.com/zerofox-oss/go-msg/backends/mem" 9 | ) 10 | 11 | func TestEncoder(t *testing.T) { 12 | c := make(chan *msg.Message, 2) 13 | 14 | // setup topics 15 | t1 := mem.Topic{C: c} 16 | t2 := Encoder(&t1) 17 | 18 | w := t2.NewWriter(context.Background()) 19 | w.Write([]byte("hello,")) 20 | w.Write([]byte("world!")) 21 | w.Close() 22 | 23 | m := <-c 24 | body, err := msg.DumpBody(m) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | encodedMsg := "aGVsbG8sd29ybGQh" 30 | if string(body) != encodedMsg { 31 | t.Fatalf("got %s expected %s", string(body), encodedMsg) 32 | } 33 | } 34 | 35 | // Tests that an base64 MessageWriter can be only be used once 36 | func TestEncoder_SingleUse(t *testing.T) { 37 | c := make(chan *msg.Message, 2) 38 | 39 | // setup topics 40 | t1 := mem.Topic{C: c} 41 | t2 := Encoder(&t1) 42 | 43 | w := t2.NewWriter(context.Background()) 44 | w.Write([]byte("dont try to use this twice!")) 45 | w.Close() 46 | 47 | m := <-c 48 | body, err := msg.DumpBody(m) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | encodedMsg := "ZG9udCB0cnkgdG8gdXNlIHRoaXMgdHdpY2Uh" 54 | if string(body) != encodedMsg { 55 | t.Fatalf("got %s expected %s", string(body), encodedMsg) 56 | } 57 | 58 | if _, err := w.Write([]byte("this will fail!!!")); err != msg.ErrClosedMessageWriter { 59 | t.Errorf("expected ErrClosedMessageWriter, got %v", err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /decorators/lz4/receiver.go: -------------------------------------------------------------------------------- 1 | package lz4 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pierrec/lz4/v4" 7 | "github.com/zerofox-oss/go-msg" 8 | ) 9 | 10 | // Decoder wraps a msg.Receiver with lz4 decoding functionality. 11 | // It only attempts to decode the Message.Body if Content-Encoding 12 | // is set to lz4. This should be used in conjuction with the base64 13 | // decode decorator, when the message queue doesn't support binary. 14 | // In this case the base64 decorator should be the outermost decorator 15 | // in order to run first. 16 | func Decoder(next msg.Receiver) msg.Receiver { 17 | return msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { 18 | if isLz4Compressed(m) { 19 | m.Body = lz4.NewReader(m.Body) 20 | } 21 | return next.Receive(ctx, m) 22 | }) 23 | } 24 | 25 | // isLz4Compressedreturns true if Content-Encoding is set to 26 | // "lz4" in the passed Message's Attributes. 27 | // 28 | // Note: MIMEHeader.Get() is used to fetch this value. In the case of a list of 29 | // values, .Get() returns the 0th value. 30 | func isLz4Compressed(m *msg.Message) bool { 31 | return m.Attributes.Get("Content-Encoding") == "lz4" 32 | } 33 | -------------------------------------------------------------------------------- /decorators/lz4/receiver_test.go: -------------------------------------------------------------------------------- 1 | package lz4 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/zerofox-oss/go-msg" 10 | ) 11 | 12 | type ChanReceiver struct { 13 | c chan *msg.Message 14 | } 15 | 16 | func (r ChanReceiver) Receive(ctx context.Context, m *msg.Message) error { 17 | r.c <- m 18 | return nil 19 | } 20 | 21 | // Tests that when a Receiver is wrapped by Decoder, the body of a message 22 | // is decoded when the Content-Compression header is set to lz4. 23 | func TestDecoder_SuccessfullyDecodesWhenHeaderIsSet(t *testing.T) { 24 | testFinish := make(chan struct{}) 25 | msgChan := make(chan *msg.Message) 26 | r := Decoder(ChanReceiver{ 27 | c: msgChan, 28 | }) 29 | 30 | ctx, cancel := context.WithCancel(context.Background()) 31 | defer cancel() 32 | 33 | // Construct a message with lz4 compressed 34 | m := &msg.Message{ 35 | Body: bytes.NewBuffer([]byte{0x4, 0x22, 0x4d, 0x18, 0x64, 0x70, 0xb9, 0x29, 0x0, 0x0, 0x0, 0x11, 0x61, 0x1, 0x0, 0xef, 0x62, 0x62, 0x62, 0x62, 0x62, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x31, 0x32, 0x33, 0x14, 0x0, 0x1, 0x0, 0x14, 0x0, 0x0, 0x28, 0x0, 0xc0, 0x62, 0x62, 0x62, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x31, 0x32, 0x33, 0x0, 0x0, 0x0, 0x0, 0x30, 0x28, 0x73, 0x84}), 36 | Attributes: msg.Attributes{}, 37 | } 38 | m.Attributes.Set("Content-Encoding", "lz4") 39 | 40 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 41 | go func() { 42 | result := <-msgChan 43 | 44 | expectedBody := "aaaaaabbbbbcccccc123aaaaaabbbbbcccccc123aaaaaabbbbbcccccc123" 45 | actual, _ := ioutil.ReadAll(result.Body) 46 | if string(actual) != expectedBody { 47 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 48 | } 49 | 50 | testFinish <- struct{}{} 51 | }() 52 | 53 | // Receive the message! 54 | err := r.Receive(ctx, m) 55 | if err != nil { 56 | t.Error(err) 57 | return 58 | } 59 | <-testFinish 60 | } 61 | 62 | // Tests that when a Receiver is wrapped by Decoder, the body of a message 63 | // is not changed if Content-Compression is not set to lz4. 64 | func TestDecoder_DoesNotModifyMessageWithoutAppropriateHeader(t *testing.T) { 65 | testFinish := make(chan struct{}) 66 | msgChan := make(chan *msg.Message) 67 | r := Decoder(ChanReceiver{ 68 | c: msgChan, 69 | }) 70 | 71 | ctx, cancel := context.WithCancel(context.Background()) 72 | defer cancel() 73 | 74 | // Construct a message without lz4 encoding 75 | m := &msg.Message{ 76 | Body: bytes.NewBufferString("abc123"), 77 | Attributes: msg.Attributes{}, 78 | } 79 | 80 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 81 | go func() { 82 | result := <-msgChan 83 | 84 | expectedBody := "abc123" 85 | actual, _ := ioutil.ReadAll(result.Body) 86 | if string(actual) != expectedBody { 87 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 88 | } 89 | 90 | testFinish <- struct{}{} 91 | }() 92 | 93 | // Receive the message! 94 | err := r.Receive(ctx, m) 95 | if err != nil { 96 | t.Error(err) 97 | return 98 | } 99 | <-testFinish 100 | } 101 | 102 | // Tests that isLz4Compressed properly identifies Content-Compression 103 | func TestIsLz4Compressed_True(t *testing.T) { 104 | m := &msg.Message{ 105 | Attributes: msg.Attributes{}, 106 | } 107 | m.Attributes.Set("Content-Encoding", "lz4") 108 | 109 | if !isLz4Compressed(m) { 110 | t.Error("Expected m to be lz4 compressed but got false.") 111 | } 112 | } 113 | 114 | // Tests that isLz4Compressed returns false if Content-Compression is 115 | // set to a value other than lz4. 116 | func TestIsLz4Compressed_FalseOtherValueInContentCompression(t *testing.T) { 117 | m := &msg.Message{ 118 | Attributes: msg.Attributes{}, 119 | } 120 | m.Attributes.Set("Content-Encoding", "gzip") 121 | 122 | if isLz4Compressed(m) { 123 | t.Error("Expected m not to be lz4 compressed but got true.") 124 | } 125 | } 126 | 127 | // Tests that isLz4Compressed returns false if Content-Compression is 128 | // not set. 129 | func TestIsLz4Compressed_FalseContentCompressionNotSet(t *testing.T) { 130 | m := &msg.Message{ 131 | Attributes: msg.Attributes{}, 132 | } 133 | m.Attributes.Set("Other-Header", "lz4") 134 | 135 | if isLz4Compressed(m) { 136 | t.Error("Expected m not to be lz4 compressed but got true.") 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /decorators/lz4/topic.go: -------------------------------------------------------------------------------- 1 | package lz4 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/pierrec/lz4/v4" 9 | "github.com/zerofox-oss/go-msg" 10 | ) 11 | 12 | // Encoder wraps a topic with another which lz4-encodes a Message. 13 | // This should used in conjunction with the base64 encoder 14 | // if the underlying message queue does not support binary (eg SQS) 15 | func Encoder(next msg.Topic) msg.Topic { 16 | options := []lz4.Option{ 17 | lz4.CompressionLevelOption(lz4.CompressionLevel(lz4.Level3)), 18 | } 19 | 20 | return msg.TopicFunc(func(ctx context.Context) msg.MessageWriter { 21 | nextW := next.NewWriter(ctx) 22 | writer := lz4.NewWriter(nextW) 23 | err := writer.Apply(options...) 24 | if err != nil { 25 | panic(err) 26 | } 27 | return &encodeWriter{ 28 | Next: nextW, 29 | Writer: writer, 30 | } 31 | }) 32 | } 33 | 34 | type encodeWriter struct { 35 | Next msg.MessageWriter 36 | 37 | Writer *lz4.Writer 38 | closed bool 39 | mux sync.Mutex 40 | } 41 | 42 | // Attributes returns the attributes associated with the MessageWriter. 43 | func (w *encodeWriter) Attributes() *msg.Attributes { 44 | return w.Next.Attributes() 45 | } 46 | 47 | func (w *encodeWriter) SetDelay(delay time.Duration) { 48 | w.Next.SetDelay(delay) 49 | } 50 | 51 | // Close calls Close on the lz4 writer before 52 | // writing bytes to the next MessageWriter. 53 | func (w *encodeWriter) Close() error { 54 | w.mux.Lock() 55 | defer w.mux.Unlock() 56 | 57 | if w.closed { 58 | return msg.ErrClosedMessageWriter 59 | } 60 | w.closed = true 61 | 62 | attrs := *w.Attributes() 63 | attrs["Content-Encoding"] = []string{"lz4"} 64 | 65 | if err := w.Writer.Close(); err != nil { 66 | return err 67 | } 68 | 69 | return w.Next.Close() 70 | } 71 | 72 | // Write writes bytes to lz4 Writer 73 | func (w *encodeWriter) Write(b []byte) (int, error) { 74 | w.mux.Lock() 75 | defer w.mux.Unlock() 76 | 77 | if w.closed { 78 | return 0, msg.ErrClosedMessageWriter 79 | } 80 | return w.Writer.Write(b) 81 | } 82 | -------------------------------------------------------------------------------- /decorators/lz4/topic_test.go: -------------------------------------------------------------------------------- 1 | package lz4 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | 8 | msg "github.com/zerofox-oss/go-msg" 9 | "github.com/zerofox-oss/go-msg/backends/mem" 10 | ) 11 | 12 | func TestEncoder(t *testing.T) { 13 | c := make(chan *msg.Message, 2) 14 | 15 | // setup topics 16 | t1 := mem.Topic{C: c} 17 | t2 := Encoder(&t1) 18 | 19 | w := t2.NewWriter(context.Background()) 20 | w.Write([]byte("hello,")) 21 | w.Write([]byte("world!")) 22 | w.Close() 23 | 24 | m := <-c 25 | body, err := msg.DumpBody(m) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | encodedMsg := []byte{0x4, 0x22, 0x4d, 0x18, 0x64, 0x70, 0xb9, 0xc, 0x0, 0x0, 0x80, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x0, 0x0, 0x0, 0x0, 0xef, 0x1f, 0xf7, 0xe4} 31 | if bytes.Compare(body, encodedMsg) != 0 { 32 | t.Fatalf("got %#v expected %#v", body, encodedMsg) 33 | } 34 | } 35 | 36 | // Tests that an lz4 MessageWriter can be only be used once 37 | func TestEncoder_SingleUse(t *testing.T) { 38 | c := make(chan *msg.Message, 2) 39 | 40 | // setup topics 41 | t1 := mem.Topic{C: c} 42 | t2 := Encoder(&t1) 43 | 44 | w := t2.NewWriter(context.Background()) 45 | w.Write([]byte("dont try to use this twice!")) 46 | w.Close() 47 | 48 | m := <-c 49 | body, err := msg.DumpBody(m) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | encodedMsg := []byte{0x4, 0x22, 0x4d, 0x18, 0x64, 0x70, 0xb9, 0x1b, 0x0, 0x0, 0x80, 0x64, 0x6f, 0x6e, 0x74, 0x20, 0x74, 0x72, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x75, 0x73, 0x65, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x74, 0x77, 0x69, 0x63, 0x65, 0x21, 0x0, 0x0, 0x0, 0x0, 0xa1, 0x89, 0xb0, 0xd9} 55 | if bytes.Compare(body, encodedMsg) != 0 { 56 | t.Fatalf("got %#v expected %#v", body, encodedMsg) 57 | } 58 | 59 | if _, err := w.Write([]byte("this will fail!!!")); err != msg.ErrClosedMessageWriter { 60 | t.Errorf("expected ErrClosedMessageWriter, got %v", err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /decorators/otel/tracing/doc.go: -------------------------------------------------------------------------------- 1 | // Tracing provides decorators which enable distributed tracing 2 | // 3 | // # How it works 4 | // 5 | // This package provides two decorators which can be used to 6 | // propagate tracing information. The topic decorator "tracing.Topic" 7 | // will automatically attach tracing information to any outgoing 8 | // messages. If no parent trace exists, it will create one automatically. 9 | // The second decorator, tracing.Receiver is used to decode tracing information 10 | // into the context.Context object which is passed into the receiver that you 11 | // provide handle messages. Again if to trace is present a trace is started and 12 | // set in the context. 13 | // 14 | // # Examples 15 | // 16 | // Using the tracing.Topic: 17 | // 18 | // func ExampleTopic() { 19 | // // make a concrete topic eg SNS 20 | // topic, _ := sns.NewTopic("arn://sns:xxx") 21 | // // make a tracing topic with the span name "msg.Writer" 22 | // topic := tracing.TracingTopic(topic, tracing.WithSpanName("msg.Writer")) 23 | // // use topic as you would without tracing 24 | // } 25 | // 26 | // Using the tracing.Receiver: 27 | // 28 | // func ExampleReceiver() { 29 | // receiver := msg.Receiver(func(ctx context.Context, m *msg.Message) error { 30 | // // your receiver implementation 31 | // // ctx will contain tracing information 32 | // // once decorated 33 | // }) 34 | // receiver := tracing.Receiver(receiver) 35 | // // use receiver as you would without tracing 36 | // } 37 | package tracing 38 | -------------------------------------------------------------------------------- /decorators/otel/tracing/receiver.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | 7 | "github.com/zerofox-oss/go-msg" 8 | "go.opencensus.io/trace/propagation" 9 | "go.opentelemetry.io/otel" 10 | ocbridge "go.opentelemetry.io/otel/bridge/opencensus" 11 | "go.opentelemetry.io/otel/trace" 12 | ) 13 | 14 | var tracer = otel.Tracer("github.com/zerofox-oss/go-msg/decorators/otel") 15 | 16 | const ( 17 | traceContextKey = "Tracecontext" 18 | traceStateKey = "Tracestate" 19 | ) 20 | 21 | type Options struct { 22 | SpanName string 23 | StartOptions trace.SpanStartOption 24 | OnlyOtel bool 25 | } 26 | 27 | type Option func(*Options) 28 | 29 | func WithSpanName(spanName string) Option { 30 | return func(o *Options) { 31 | o.SpanName = spanName 32 | } 33 | } 34 | 35 | func WithStartOption(so trace.SpanStartOption) Option { 36 | return func(o *Options) { 37 | o.StartOptions = so 38 | } 39 | } 40 | 41 | func WithOnlyOtel(onlyOtel bool) Option { 42 | return func(o *Options) { 43 | o.OnlyOtel = onlyOtel 44 | } 45 | } 46 | 47 | // Receiver Wraps another msg.Receiver, populating 48 | // the context with any upstream tracing information. 49 | func Receiver(next msg.Receiver, opts ...Option) msg.Receiver { 50 | options := &Options{ 51 | SpanName: "msg.Receiver", 52 | } 53 | 54 | for _, opt := range opts { 55 | opt(options) 56 | } 57 | 58 | return msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { 59 | ctx, span := withContext(ctx, m, options) 60 | defer span.End() 61 | return next.Receive(ctx, m) 62 | }) 63 | } 64 | 65 | // withContext checks to see if a traceContext is 66 | // present in the message attributes. If one is present 67 | // a new span is created with that tracecontext as the parent 68 | // otherwise a new span is created without a parent. A new context 69 | // which contains the created span well as the span itself 70 | // is returned 71 | func withContext(ctx context.Context, m *msg.Message, options *Options) (context.Context, trace.Span) { 72 | textCarrier := msgAttributesTextCarrier{attributes: &m.Attributes} 73 | tmprop := otel.GetTextMapPropagator() 74 | 75 | // if any of the fields used by 76 | // the text map propagation is set 77 | // we use otel to decode 78 | for _, field := range tmprop.Fields() { 79 | if m.Attributes.Get(field) != "" { 80 | ctx = tmprop.Extract(ctx, textCarrier) 81 | return tracer.Start(ctx, options.SpanName) 82 | } 83 | } 84 | 85 | // if we are set to use only otel 86 | // do not fall back to opencensus 87 | if options.OnlyOtel { 88 | return tracer.Start(ctx, options.SpanName) 89 | } 90 | 91 | // fallback to old behaviour (opencensus) if we don't 92 | // receive any otel headers 93 | traceContextB64 := m.Attributes.Get(traceContextKey) 94 | if traceContextB64 == "" { 95 | return tracer.Start(ctx, options.SpanName) 96 | } 97 | 98 | traceContext, err := base64.StdEncoding.DecodeString(traceContextB64) 99 | if err != nil { 100 | return tracer.Start(ctx, options.SpanName) 101 | } 102 | 103 | spanContext, ok := propagation.FromBinary(traceContext) 104 | if !ok { 105 | return tracer.Start(ctx, options.SpanName) 106 | } 107 | 108 | traceStateString := m.Attributes.Get(traceStateKey) 109 | if traceStateString != "" { 110 | ts := tracestateFromString(traceStateString) 111 | spanContext.Tracestate = ts 112 | } 113 | 114 | // convert the opencensus span context to otel 115 | otelSpanContext := ocbridge.OCSpanContextToOTel(spanContext) 116 | if !otelSpanContext.IsValid() { 117 | return tracer.Start(ctx, options.SpanName) 118 | } 119 | 120 | return tracer.Start(trace.ContextWithRemoteSpanContext(ctx, otelSpanContext), options.SpanName) 121 | } 122 | -------------------------------------------------------------------------------- /decorators/otel/tracing/receiver_test.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "encoding/base64" 8 | "io/ioutil" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | "github.com/zerofox-oss/go-msg" 13 | "go.opencensus.io/trace" 14 | "go.opencensus.io/trace/propagation" 15 | ocbridge "go.opentelemetry.io/otel/bridge/opencensus" 16 | oteltrace "go.opentelemetry.io/otel/trace" 17 | ) 18 | 19 | type msgWithContext struct { 20 | msg *msg.Message 21 | ctx context.Context 22 | } 23 | 24 | type ChanReceiver struct { 25 | c chan msgWithContext 26 | } 27 | 28 | func (r ChanReceiver) Receive(ctx context.Context, m *msg.Message) error { 29 | r.c <- msgWithContext{msg: m, ctx: ctx} 30 | return nil 31 | } 32 | 33 | func makeOCSpanContext() (trace.SpanContext, string) { 34 | b := make([]byte, 24) 35 | rand.Read(b) 36 | 37 | var tid [16]byte 38 | var sid [8]byte 39 | 40 | copy(tid[:], b[:16]) 41 | copy(sid[:], b[:8]) 42 | 43 | sc := trace.SpanContext{ 44 | TraceID: tid, 45 | SpanID: sid, 46 | } 47 | 48 | b64 := base64.StdEncoding.EncodeToString(propagation.Binary(sc)) 49 | return sc, b64 50 | } 51 | 52 | // Tests that when a Receiver is wrapped by TracingReceiver, and tracecontext 53 | // is present, a OC span is started is started and set in the receive context with the correct 54 | // parent context 55 | func TestDecoder_SuccessfullyDecodesSpanWhenTraceContextIsPresent(t *testing.T) { 56 | testFinish := make(chan struct{}) 57 | msgChan := make(chan msgWithContext) 58 | r := Receiver(ChanReceiver{ 59 | c: msgChan, 60 | }) 61 | 62 | ctx, cancel := context.WithCancel(context.Background()) 63 | defer cancel() 64 | 65 | sc, b64Sc := makeOCSpanContext() 66 | 67 | // Construct a message with base64 encoding (YWJjMTIz == abc123) 68 | m := &msg.Message{ 69 | Body: bytes.NewBufferString("hello"), 70 | Attributes: msg.Attributes{}, 71 | } 72 | m.Attributes.Set("Tracecontext", b64Sc) 73 | 74 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 75 | go func() { 76 | result := <-msgChan 77 | 78 | expectedBody := "hello" 79 | actual, _ := ioutil.ReadAll(result.msg.Body) 80 | if string(actual) != expectedBody { 81 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 82 | } 83 | 84 | span := oteltrace.SpanFromContext(result.ctx) 85 | if span == nil { 86 | t.Errorf("span was not expected to be nil") 87 | } 88 | 89 | receivedSC := span.SpanContext() 90 | ocReceivedSC := ocbridge.OTelSpanContextToOC(receivedSC) 91 | 92 | if ocReceivedSC.TraceID != sc.TraceID { 93 | t.Errorf(cmp.Diff(receivedSC.TraceID, sc.TraceID)) 94 | } 95 | 96 | testFinish <- struct{}{} 97 | }() 98 | 99 | // Receive the message! 100 | err := r.Receive(ctx, m) 101 | if err != nil { 102 | t.Error(err) 103 | return 104 | } 105 | <-testFinish 106 | } 107 | 108 | // Tests that when a Receiver is wrapped by a Tracing Receiver, and 109 | // the message does not contain a tracecontext, a new span is created 110 | func TestDecoder_SuccessfullySetsSpanWhenNoTraceContext(t *testing.T) { 111 | testFinish := make(chan struct{}) 112 | msgChan := make(chan msgWithContext) 113 | r := Receiver(ChanReceiver{ 114 | c: msgChan, 115 | }) 116 | 117 | ctx, cancel := context.WithCancel(context.Background()) 118 | defer cancel() 119 | 120 | // Construct a message without base64 encoding 121 | m := &msg.Message{ 122 | Body: bytes.NewBufferString("abc123"), 123 | Attributes: msg.Attributes{}, 124 | } 125 | 126 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 127 | go func() { 128 | result := <-msgChan 129 | expectedBody := "abc123" 130 | actual, _ := ioutil.ReadAll(result.msg.Body) 131 | if string(actual) != expectedBody { 132 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 133 | } 134 | 135 | span := oteltrace.SpanFromContext(result.ctx) 136 | if span == nil { 137 | t.Errorf("span was not expected to be nil") 138 | } 139 | 140 | testFinish <- struct{}{} 141 | }() 142 | 143 | // Receive the message! 144 | err := r.Receive(ctx, m) 145 | if err != nil { 146 | t.Error(err) 147 | return 148 | } 149 | <-testFinish 150 | } 151 | 152 | // Tests that when a Receiver is wrapped by a Tracing Receiver, and 153 | // the message contains an invalid b64 encodeded tracecontext, a span 154 | // is still sucessfully set 155 | func TestDecoder_SuccessfullySetsSpanWhenInvalidTraceContextB64(t *testing.T) { 156 | testFinish := make(chan struct{}) 157 | msgChan := make(chan msgWithContext) 158 | r := Receiver(ChanReceiver{ 159 | c: msgChan, 160 | }) 161 | 162 | ctx, cancel := context.WithCancel(context.Background()) 163 | defer cancel() 164 | 165 | // Construct a message without base64 encoding 166 | m := &msg.Message{ 167 | Body: bytes.NewBufferString("abc123"), 168 | Attributes: msg.Attributes{}, 169 | } 170 | 171 | m.Attributes.Set("Tracecontext", "invalidcontext") 172 | 173 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 174 | go func() { 175 | result := <-msgChan 176 | expectedBody := "abc123" 177 | actual, _ := ioutil.ReadAll(result.msg.Body) 178 | if string(actual) != expectedBody { 179 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 180 | } 181 | 182 | span := oteltrace.SpanFromContext(result.ctx) 183 | if span == nil { 184 | t.Errorf("span was not expected to be nil") 185 | } 186 | 187 | testFinish <- struct{}{} 188 | }() 189 | 190 | // Receive the message! 191 | err := r.Receive(ctx, m) 192 | if err != nil { 193 | t.Error(err) 194 | return 195 | } 196 | <-testFinish 197 | } 198 | 199 | // Tests that when a Receiver is wrapped by a Tracing Receiver, and 200 | // the message contains an invalid binary encodeded tracecontext, a span 201 | // is still sucessfully set 202 | func TestDecoder_SuccessfullySetsSpanWhenInvalidTraceContextBinary(t *testing.T) { 203 | testFinish := make(chan struct{}) 204 | msgChan := make(chan msgWithContext) 205 | r := Receiver(ChanReceiver{ 206 | c: msgChan, 207 | }) 208 | 209 | ctx, cancel := context.WithCancel(context.Background()) 210 | defer cancel() 211 | 212 | // Construct a message without base64 encoding 213 | m := &msg.Message{ 214 | Body: bytes.NewBufferString("abc123"), 215 | Attributes: msg.Attributes{}, 216 | } 217 | 218 | // "YWJjMTIz" is valid b64 219 | m.Attributes.Set("Tracecontext", "YWJjMTIz") 220 | 221 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 222 | go func() { 223 | result := <-msgChan 224 | expectedBody := "abc123" 225 | actual, _ := ioutil.ReadAll(result.msg.Body) 226 | if string(actual) != expectedBody { 227 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 228 | } 229 | 230 | span := oteltrace.SpanFromContext(result.ctx) 231 | if span == nil { 232 | t.Errorf("span was not expected to be nil") 233 | } 234 | 235 | testFinish <- struct{}{} 236 | }() 237 | 238 | // Receive the message! 239 | err := r.Receive(ctx, m) 240 | if err != nil { 241 | t.Error(err) 242 | return 243 | } 244 | <-testFinish 245 | } 246 | -------------------------------------------------------------------------------- /decorators/otel/tracing/topic.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/zerofox-oss/go-msg" 12 | "go.opencensus.io/trace/propagation" 13 | "go.opentelemetry.io/otel" 14 | "go.opentelemetry.io/otel/attribute" 15 | ocbridge "go.opentelemetry.io/otel/bridge/opencensus" 16 | "go.opentelemetry.io/otel/trace" 17 | ) 18 | 19 | func msgAttributesToTrace(msgAttributes msg.Attributes) []attribute.KeyValue { 20 | traceAttributes := make([]attribute.KeyValue, len(msgAttributes)) 21 | for key, value := range msgAttributes { 22 | traceAttributes = append(traceAttributes, attribute.String(key, strings.Join(value, ";"))) 23 | } 24 | return traceAttributes 25 | } 26 | 27 | type msgAttributesTextCarrier struct { 28 | attributes *msg.Attributes 29 | } 30 | 31 | func (m msgAttributesTextCarrier) Get(key string) string { 32 | return m.attributes.Get(key) 33 | } 34 | 35 | func (m msgAttributesTextCarrier) Set(key string, value string) { 36 | m.attributes.Set(key, value) 37 | } 38 | 39 | func (m msgAttributesTextCarrier) Keys() []string { 40 | keys := []string{} 41 | for key := range *m.attributes { 42 | keys = append(keys, key) 43 | } 44 | return keys 45 | } 46 | 47 | // Topic wraps a msg.Topic, attaching any tracing data 48 | // via msg.Attributes to send downstream 49 | func Topic(next msg.Topic, opts ...Option) msg.Topic { 50 | options := &Options{ 51 | SpanName: "msg.MessageWriter", 52 | } 53 | 54 | for _, opt := range opts { 55 | opt(options) 56 | } 57 | 58 | return msg.TopicFunc(func(ctx context.Context) msg.MessageWriter { 59 | tracingCtx, span := tracer.Start( 60 | ctx, 61 | options.SpanName, 62 | trace.WithSpanKind(trace.SpanKindProducer), 63 | ) 64 | 65 | return &tracingWriter{ 66 | Next: next.NewWriter(tracingCtx), 67 | ctx: tracingCtx, 68 | onClose: span.End, 69 | options: options, 70 | } 71 | }) 72 | } 73 | 74 | type tracingWriter struct { 75 | Next msg.MessageWriter 76 | 77 | buf bytes.Buffer 78 | closed bool 79 | mux sync.Mutex 80 | ctx context.Context 81 | 82 | // onClose is used to end the span 83 | // and send data to tracing framework 84 | onClose func(...trace.SpanEndOption) 85 | 86 | options *Options 87 | } 88 | 89 | // Attributes returns the attributes associated with the MessageWriter. 90 | func (w *tracingWriter) Attributes() *msg.Attributes { 91 | return w.Next.Attributes() 92 | } 93 | 94 | func (w *tracingWriter) SetDelay(delay time.Duration) { 95 | w.Next.SetDelay(delay) 96 | } 97 | 98 | // Close adds tracing message attributes 99 | // writing to the next MessageWriter. 100 | func (w *tracingWriter) Close() error { 101 | w.mux.Lock() 102 | defer w.mux.Unlock() 103 | defer w.onClose() 104 | 105 | if w.closed { 106 | return msg.ErrClosedMessageWriter 107 | } 108 | w.closed = true 109 | 110 | dataToWrite := w.buf.Bytes() 111 | 112 | if span := trace.SpanFromContext(w.ctx); span != nil { 113 | // set message attributes 114 | // as span tags for debugging 115 | attrs := *w.Attributes() 116 | span.SetAttributes(msgAttributesToTrace(attrs)...) 117 | 118 | // we use the global text map propagator 119 | // to set string values onto the message 120 | // attributes, this will set the headers 121 | // in the new style tracecontext format 122 | textCarrier := msgAttributesTextCarrier{attributes: w.Attributes()} 123 | tmprop := otel.GetTextMapPropagator() 124 | tmprop.Inject(w.ctx, textCarrier) 125 | 126 | // also send opencensus headers 127 | // for backwards compatiblity 128 | if !w.options.OnlyOtel { 129 | // we need to convert the otel 130 | // span to the old style opencensus 131 | sc := span.SpanContext() 132 | ocSpan := ocbridge.OTelSpanContextToOC(sc) 133 | bs := propagation.Binary(ocSpan) 134 | traceBuf := make([]byte, base64.StdEncoding.EncodedLen(len(bs))) 135 | base64.StdEncoding.Encode(traceBuf, bs) 136 | 137 | attrs.Set(traceContextKey, string(traceBuf)) 138 | tracestateString := tracestateToString(ocSpan) 139 | if tracestateString != "" { 140 | attrs.Set(traceStateKey, tracestateToString(ocSpan)) 141 | } 142 | } 143 | } 144 | 145 | if _, err := w.Next.Write(dataToWrite); err != nil { 146 | return err 147 | } 148 | return w.Next.Close() 149 | } 150 | 151 | // Write writes bytes to an internal buffer. 152 | func (w *tracingWriter) Write(b []byte) (int, error) { 153 | w.mux.Lock() 154 | defer w.mux.Unlock() 155 | 156 | if w.closed { 157 | return 0, msg.ErrClosedMessageWriter 158 | } 159 | return w.buf.Write(b) 160 | } 161 | -------------------------------------------------------------------------------- /decorators/otel/tracing/topic_test.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "testing" 7 | 8 | msg "github.com/zerofox-oss/go-msg" 9 | "github.com/zerofox-oss/go-msg/backends/mem" 10 | ocprop "go.opencensus.io/trace/propagation" 11 | "go.opentelemetry.io/otel" 12 | "go.opentelemetry.io/otel/propagation" 13 | tracesdk "go.opentelemetry.io/otel/sdk/trace" 14 | "go.opentelemetry.io/otel/trace" 15 | ) 16 | 17 | func TestTopic__SucessfullyInsertsTraceContext(t *testing.T) { 18 | tp := tracesdk.NewTracerProvider() 19 | otel.SetTextMapPropagator(propagation.TraceContext{}) 20 | otel.SetTracerProvider(tp) 21 | 22 | t.Cleanup(func() { 23 | tp.Shutdown(context.Background()) 24 | }) 25 | c := make(chan *msg.Message, 2) 26 | 27 | // setup topics 28 | t2 := Topic(&mem.Topic{C: c}, WithSpanName("something.Different")) 29 | 30 | w := t2.NewWriter(context.Background()) 31 | w.Write([]byte("hello,")) 32 | w.Write([]byte("world!")) 33 | w.Close() 34 | 35 | m := <-c 36 | body, err := msg.DumpBody(m) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | expectedBody := "hello,world!" 42 | if string(body) != expectedBody { 43 | t.Fatalf("got %s expected %s", string(body), expectedBody) 44 | } 45 | 46 | tc := m.Attributes.Get("Tracecontext") 47 | if tc == "" { 48 | t.Fatalf("expected tracecontext attribute to be set") 49 | } 50 | 51 | b, err := base64.StdEncoding.DecodeString(tc) 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | 56 | _, ok := ocprop.FromBinary(b) 57 | if !ok { 58 | t.Errorf("expected spanContext to be decoded from tracecontext attribute") 59 | } 60 | } 61 | 62 | func TestTopic__SucessfullyInsertsOtelContext(t *testing.T) { 63 | tp := tracesdk.NewTracerProvider() 64 | otel.SetTextMapPropagator(propagation.TraceContext{}) 65 | otel.SetTracerProvider(tp) 66 | 67 | t.Cleanup(func() { 68 | tp.Shutdown(context.Background()) 69 | }) 70 | c := make(chan *msg.Message, 2) 71 | 72 | // setup topics 73 | t2 := Topic(&mem.Topic{C: c}, WithSpanName("something.Different")) 74 | 75 | w := t2.NewWriter(context.Background()) 76 | w.Write([]byte("hello,")) 77 | w.Write([]byte("world!")) 78 | w.Close() 79 | 80 | m := <-c 81 | body, err := msg.DumpBody(m) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | expectedBody := "hello,world!" 87 | if string(body) != expectedBody { 88 | t.Fatalf("got %s expected %s", string(body), expectedBody) 89 | } 90 | 91 | traceparent := m.Attributes.Get("Traceparent") 92 | if traceparent == "" { 93 | t.Fatalf("expected traceparent attribute to be set") 94 | } 95 | 96 | textMapProp := otel.GetTextMapPropagator() 97 | ctx := textMapProp.Extract(context.Background(), msgAttributesTextCarrier{attributes: &m.Attributes}) 98 | span := trace.SpanFromContext(ctx) 99 | if !span.SpanContext().IsValid() { 100 | t.Fatalf("expected otel span to be valid") 101 | } 102 | 103 | if !span.SpanContext().IsRemote() { 104 | t.Fatalf("expected otel span to be remote") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /decorators/otel/tracing/tracing_test.go: -------------------------------------------------------------------------------- 1 | package tracing_test 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/zerofox-oss/go-msg" 9 | "github.com/zerofox-oss/go-msg/backends/mem" 10 | "github.com/zerofox-oss/go-msg/decorators/otel/tracing" 11 | octracing "github.com/zerofox-oss/go-msg/decorators/tracing" 12 | octrace "go.opencensus.io/trace" 13 | "go.opentelemetry.io/otel" 14 | "go.opentelemetry.io/otel/propagation" 15 | tracesdk "go.opentelemetry.io/otel/sdk/trace" 16 | "go.opentelemetry.io/otel/trace" 17 | ) 18 | 19 | type message struct { 20 | ctx context.Context 21 | m *msg.Message 22 | } 23 | 24 | func startOTSpan() (context.Context, string, func()) { 25 | ctx := context.Background() 26 | ctx, span := otel.Tracer("tracing_test").Start(ctx, "WriteMsg") 27 | return ctx, span.SpanContext().TraceID().String(), func() { 28 | span.End() 29 | } 30 | } 31 | 32 | func startOCSpan() (context.Context, string, func()) { 33 | ctx := context.Background() 34 | ctx, span := octrace.StartSpan(ctx, "WriteMsg") 35 | return ctx, span.SpanContext().TraceID.String(), func() { 36 | span.End() 37 | } 38 | } 39 | 40 | func readOCSpan(ctx context.Context) string { 41 | span := octrace.FromContext(ctx) 42 | if span != nil { 43 | return span.SpanContext().TraceID.String() 44 | } 45 | return "" 46 | } 47 | 48 | func readOTSpan(ctx context.Context) string { 49 | receivedSpan := trace.SpanFromContext(ctx) 50 | receivedSpanContext := receivedSpan.SpanContext() 51 | return receivedSpanContext.TraceID().String() 52 | } 53 | 54 | func verifyEncodeDecode( 55 | t *testing.T, 56 | topicDecorator func(next msg.Topic) msg.Topic, 57 | receiveDecorator func(next msg.Receiver) msg.Receiver, 58 | spanCreator func() (context.Context, string, func()), 59 | spanReader func(context.Context) string, 60 | ) { 61 | c1 := make(chan *msg.Message) 62 | topic := mem.Topic{C: c1} 63 | srv := mem.NewServer(c1, 1) 64 | 65 | var lock sync.RWMutex 66 | 67 | receivedMessages := []message{} 68 | go func() { 69 | srv.Serve(receiveDecorator(msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { 70 | lock.Lock() 71 | receivedMessages = append(receivedMessages, message{ctx: ctx, m: m}) 72 | lock.Unlock() 73 | return nil 74 | }))) 75 | }() 76 | 77 | expectedTraces := []string{} 78 | for i := 0; i < 20; i++ { 79 | ctx, traceID, end := spanCreator() 80 | writer := topicDecorator(&topic).NewWriter(ctx) 81 | writer.Write([]byte("message body")) 82 | writer.Close() 83 | end() 84 | expectedTraces = append(expectedTraces, traceID) 85 | } 86 | 87 | srv.Shutdown(context.Background()) 88 | 89 | for i, expectedTraceID := range expectedTraces { 90 | lock.RLock() 91 | m := receivedMessages[i] 92 | lock.RUnlock() 93 | traceID := spanReader(m.ctx) 94 | if traceID != expectedTraceID { 95 | t.Errorf("received span did not have the expected id %s != %s", traceID, expectedTraceID) 96 | } 97 | } 98 | } 99 | 100 | func TestTopicAndReceiverCompatability(t *testing.T) { 101 | tp := tracesdk.NewTracerProvider() 102 | 103 | otel.SetTextMapPropagator(propagation.TraceContext{}) 104 | otel.SetTracerProvider(tp) 105 | 106 | t.Cleanup(func() { 107 | tp.Shutdown(context.Background()) 108 | }) 109 | 110 | t.Run("Otel->Otel WithOnlyOtel=true", func(t *testing.T) { 111 | verifyEncodeDecode(t, 112 | func(next msg.Topic) msg.Topic { 113 | return tracing.Topic(next, tracing.WithOnlyOtel(true)) 114 | }, 115 | func(next msg.Receiver) msg.Receiver { 116 | return tracing.Receiver(next, tracing.WithOnlyOtel(true)) 117 | }, 118 | startOTSpan, 119 | readOTSpan, 120 | ) 121 | }) 122 | 123 | t.Run("Otel->Otel WithOnlyOtel=false", func(t *testing.T) { 124 | verifyEncodeDecode(t, 125 | func(next msg.Topic) msg.Topic { 126 | return tracing.Topic(next, tracing.WithOnlyOtel(false)) 127 | }, 128 | func(next msg.Receiver) msg.Receiver { 129 | return tracing.Receiver(next, tracing.WithOnlyOtel(false)) 130 | }, 131 | startOTSpan, 132 | readOTSpan, 133 | ) 134 | }) 135 | 136 | t.Run("OC->Otel", func(t *testing.T) { 137 | verifyEncodeDecode(t, 138 | func(next msg.Topic) msg.Topic { 139 | return octracing.Topic(next) 140 | }, 141 | func(next msg.Receiver) msg.Receiver { 142 | return tracing.Receiver(next, tracing.WithOnlyOtel(false)) 143 | }, 144 | startOCSpan, 145 | readOTSpan, 146 | ) 147 | }) 148 | 149 | t.Run("Otel->OC", func(t *testing.T) { 150 | verifyEncodeDecode(t, 151 | func(next msg.Topic) msg.Topic { 152 | return tracing.Topic(next, tracing.WithOnlyOtel(false)) 153 | }, 154 | func(next msg.Receiver) msg.Receiver { 155 | return octracing.Receiver(next) 156 | }, 157 | startOTSpan, 158 | readOCSpan, 159 | ) 160 | }) 161 | } 162 | -------------------------------------------------------------------------------- /decorators/otel/tracing/utils.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "go.opencensus.io/trace" 8 | "go.opencensus.io/trace/tracestate" 9 | ) 10 | 11 | // CODE BASED ON: 12 | // https://github.com/census-instrumentation/opencensus-go/blob/ \ 13 | // master/plugin/ochttp/propagation/tracecontext/propagation.go 14 | 15 | const ( 16 | trimOWSRegexFmt = `^[\x09\x20]*(.*[^\x20\x09])[\x09\x20]*$` 17 | maxTracestateLen = 512 18 | ) 19 | 20 | var trimOWSRegExp = regexp.MustCompile(trimOWSRegexFmt) // nolint 21 | 22 | func tracestateToString(sc trace.SpanContext) string { 23 | pairs := make([]string, 0, len(sc.Tracestate.Entries())) 24 | if sc.Tracestate != nil { 25 | for _, entry := range sc.Tracestate.Entries() { 26 | pairs = append(pairs, strings.Join([]string{entry.Key, entry.Value}, "=")) 27 | } 28 | return strings.Join(pairs, ",") 29 | } 30 | return "" 31 | } 32 | 33 | func tracestateFromString(tracestateString string) *tracestate.Tracestate { 34 | var entries []tracestate.Entry // nolint 35 | pairs := strings.Split(tracestateString, ",") 36 | hdrLenWithoutOWS := len(pairs) - 1 // Number of commas 37 | for _, pair := range pairs { 38 | matches := trimOWSRegExp.FindStringSubmatch(pair) 39 | if matches == nil { 40 | return nil 41 | } 42 | pair = matches[1] 43 | hdrLenWithoutOWS += len(pair) 44 | if hdrLenWithoutOWS > maxTracestateLen { 45 | return nil 46 | } 47 | kv := strings.Split(pair, "=") 48 | if len(kv) != 2 { 49 | return nil 50 | } 51 | entries = append(entries, tracestate.Entry{Key: kv[0], Value: kv[1]}) 52 | } 53 | ts, err := tracestate.New(nil, entries...) 54 | if err != nil { 55 | return nil 56 | } 57 | 58 | return ts 59 | } 60 | -------------------------------------------------------------------------------- /decorators/tracing/doc.go: -------------------------------------------------------------------------------- 1 | // Tracing provides decorators which enable distributed tracing 2 | // 3 | // # How it works 4 | // 5 | // This package provides two decorators which can be used to 6 | // propagate tracing information. The topic decorator "tracing.Topic" 7 | // will automatically attach tracing information to any outgoing 8 | // messages. If no parent trace exists, it will create one automatically. 9 | // The second decorator, tracing.Receiver is used to decode tracing information 10 | // into the context.Context object which is passed into the receiver that you 11 | // provide handle messages. Again if to trace is present a trace is started and 12 | // set in the context. 13 | // 14 | // # Examples 15 | // 16 | // Using the tracing.Topic: 17 | // 18 | // func ExampleTopic() { 19 | // // make a concrete topic eg SNS 20 | // topic, _ := sns.NewTopic("arn://sns:xxx") 21 | // // make a tracing topic with the span name "msg.Writer" 22 | // topic := tracing.TracingTopic(topic, tracing.WithSpanName("msg.Writer")) 23 | // // use topic as you would without tracing 24 | // } 25 | // 26 | // Using the tracing.Receiver: 27 | // 28 | // func ExampleReceiver() { 29 | // receiver := msg.Receiver(func(ctx context.Context, m *msg.Message) error { 30 | // // your receiver implementation 31 | // // ctx will contain tracing information 32 | // // once decorated 33 | // }) 34 | // receiver := tracing.Receiver(receiver) 35 | // // use receiver as you would without tracing 36 | // } 37 | package tracing 38 | -------------------------------------------------------------------------------- /decorators/tracing/receiver.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | 7 | "github.com/zerofox-oss/go-msg" 8 | "go.opencensus.io/trace" 9 | "go.opencensus.io/trace/propagation" 10 | ) 11 | 12 | const ( 13 | traceContextKey = "Tracecontext" 14 | traceStateKey = "Tracestate" 15 | ) 16 | 17 | type Options struct { 18 | SpanName string 19 | StartOptions trace.StartOptions 20 | } 21 | 22 | type Option func(*Options) 23 | 24 | func WithSpanName(spanName string) Option { 25 | return func(o *Options) { 26 | o.SpanName = spanName 27 | } 28 | } 29 | 30 | func WithStartOption(so trace.StartOptions) Option { 31 | return func(o *Options) { 32 | o.StartOptions = so 33 | } 34 | } 35 | 36 | // Receiver Wraps another msg.Receiver, populating 37 | // the context with any upstream tracing information. 38 | func Receiver(next msg.Receiver, opts ...Option) msg.Receiver { 39 | options := &Options{ 40 | SpanName: "msg.Receiver", 41 | StartOptions: trace.StartOptions{}, 42 | } 43 | 44 | for _, opt := range opts { 45 | opt(options) 46 | } 47 | 48 | return msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { 49 | ctx, span := withContext(ctx, m, options) 50 | defer span.End() 51 | return next.Receive(ctx, m) 52 | }) 53 | } 54 | 55 | // withContext checks to see if a traceContext is 56 | // present in the message attributes. If one is present 57 | // a new span is created with that tracecontext as the parent 58 | // otherwise a new span is created without a parent. A new context 59 | // which contains the created span well as the span itself 60 | // is returned 61 | func withContext(ctx context.Context, m *msg.Message, options *Options) (context.Context, *trace.Span) { 62 | traceContextB64 := m.Attributes.Get(traceContextKey) 63 | 64 | startOptions := options.StartOptions 65 | 66 | if traceContextB64 == "" { 67 | return trace.StartSpan(ctx, options.SpanName, trace.WithSampler(startOptions.Sampler)) 68 | } 69 | 70 | traceContext, err := base64.StdEncoding.DecodeString(traceContextB64) 71 | if err != nil { 72 | return trace.StartSpan(ctx, options.SpanName, trace.WithSampler(startOptions.Sampler)) 73 | } 74 | 75 | spanContext, ok := propagation.FromBinary(traceContext) 76 | if !ok { 77 | return trace.StartSpan(ctx, options.SpanName, trace.WithSampler(startOptions.Sampler)) 78 | } 79 | 80 | traceStateString := m.Attributes.Get(traceStateKey) 81 | if traceStateString != "" { 82 | ts := tracestateFromString(traceStateString) 83 | spanContext.Tracestate = ts 84 | } 85 | 86 | return trace.StartSpanWithRemoteParent(ctx, options.SpanName, spanContext, trace.WithSampler(startOptions.Sampler)) 87 | } 88 | -------------------------------------------------------------------------------- /decorators/tracing/receiver_test.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "encoding/base64" 8 | "io/ioutil" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | "github.com/zerofox-oss/go-msg" 13 | "go.opencensus.io/trace" 14 | "go.opencensus.io/trace/propagation" 15 | ) 16 | 17 | type msgWithContext struct { 18 | msg *msg.Message 19 | ctx context.Context 20 | } 21 | 22 | type ChanReceiver struct { 23 | c chan msgWithContext 24 | } 25 | 26 | func (r ChanReceiver) Receive(ctx context.Context, m *msg.Message) error { 27 | r.c <- msgWithContext{msg: m, ctx: ctx} 28 | return nil 29 | } 30 | 31 | func makeSpanContext() (trace.SpanContext, string) { 32 | b := make([]byte, 24) 33 | rand.Read(b) 34 | 35 | var tid [16]byte 36 | var sid [8]byte 37 | 38 | copy(tid[:], b[:16]) 39 | copy(sid[:], b[:8]) 40 | 41 | sc := trace.SpanContext{ 42 | TraceID: tid, 43 | SpanID: sid, 44 | } 45 | 46 | b64 := base64.StdEncoding.EncodeToString(propagation.Binary(sc)) 47 | return sc, b64 48 | } 49 | 50 | // Tests that when a Receiver is wrapped by TracingReceiver, and tracecontext 51 | // is present, a span is started and set in the receive context with the correct 52 | // parent context 53 | func TestDecoder_SuccessfullyDecodesSpanWhenTraceContextIsPresent(t *testing.T) { 54 | testFinish := make(chan struct{}) 55 | msgChan := make(chan msgWithContext) 56 | r := Receiver(ChanReceiver{ 57 | c: msgChan, 58 | }) 59 | 60 | ctx, cancel := context.WithCancel(context.Background()) 61 | defer cancel() 62 | 63 | sc, b64Sc := makeSpanContext() 64 | 65 | // Construct a message with base64 encoding (YWJjMTIz == abc123) 66 | m := &msg.Message{ 67 | Body: bytes.NewBufferString("hello"), 68 | Attributes: msg.Attributes{}, 69 | } 70 | m.Attributes.Set("Tracecontext", b64Sc) 71 | 72 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 73 | go func() { 74 | result := <-msgChan 75 | 76 | expectedBody := "hello" 77 | actual, _ := ioutil.ReadAll(result.msg.Body) 78 | if string(actual) != expectedBody { 79 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 80 | } 81 | 82 | span := trace.FromContext(result.ctx) 83 | if span == nil { 84 | t.Errorf("span was not expected to be nil") 85 | } 86 | 87 | receivedSC := span.SpanContext() 88 | 89 | if receivedSC.TraceID != sc.TraceID { 90 | t.Errorf(cmp.Diff(receivedSC.TraceID, sc.TraceID)) 91 | } 92 | 93 | if receivedSC.Tracestate != sc.Tracestate { 94 | t.Errorf(cmp.Diff(receivedSC.TraceID, sc.TraceID)) 95 | } 96 | 97 | testFinish <- struct{}{} 98 | }() 99 | 100 | // Receive the message! 101 | err := r.Receive(ctx, m) 102 | if err != nil { 103 | t.Error(err) 104 | return 105 | } 106 | <-testFinish 107 | } 108 | 109 | // Tests that when a Receiver is wrapped by a Tracing Receiver, and 110 | // the message does not contain a tracecontext, a new span is created 111 | func TestDecoder_SuccessfullySetsSpanWhenNoTraceContext(t *testing.T) { 112 | testFinish := make(chan struct{}) 113 | msgChan := make(chan msgWithContext) 114 | r := Receiver(ChanReceiver{ 115 | c: msgChan, 116 | }) 117 | 118 | ctx, cancel := context.WithCancel(context.Background()) 119 | defer cancel() 120 | 121 | // Construct a message without base64 encoding 122 | m := &msg.Message{ 123 | Body: bytes.NewBufferString("abc123"), 124 | Attributes: msg.Attributes{}, 125 | } 126 | 127 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 128 | go func() { 129 | result := <-msgChan 130 | expectedBody := "abc123" 131 | actual, _ := ioutil.ReadAll(result.msg.Body) 132 | if string(actual) != expectedBody { 133 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 134 | } 135 | 136 | span := trace.FromContext(result.ctx) 137 | if span == nil { 138 | t.Errorf("span was not expected to be nil") 139 | } 140 | 141 | testFinish <- struct{}{} 142 | }() 143 | 144 | // Receive the message! 145 | err := r.Receive(ctx, m) 146 | if err != nil { 147 | t.Error(err) 148 | return 149 | } 150 | <-testFinish 151 | } 152 | 153 | // Tests that when a Receiver is wrapped by a Tracing Receiver, and 154 | // the message contains an invalid b64 encodeded tracecontext, a span 155 | // is still sucessfully set 156 | func TestDecoder_SuccessfullySetsSpanWhenInvalidTraceContextB64(t *testing.T) { 157 | testFinish := make(chan struct{}) 158 | msgChan := make(chan msgWithContext) 159 | r := Receiver(ChanReceiver{ 160 | c: msgChan, 161 | }) 162 | 163 | ctx, cancel := context.WithCancel(context.Background()) 164 | defer cancel() 165 | 166 | // Construct a message without base64 encoding 167 | m := &msg.Message{ 168 | Body: bytes.NewBufferString("abc123"), 169 | Attributes: msg.Attributes{}, 170 | } 171 | 172 | m.Attributes.Set("Tracecontext", "invalidcontext") 173 | 174 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 175 | go func() { 176 | result := <-msgChan 177 | expectedBody := "abc123" 178 | actual, _ := ioutil.ReadAll(result.msg.Body) 179 | if string(actual) != expectedBody { 180 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 181 | } 182 | 183 | span := trace.FromContext(result.ctx) 184 | if span == nil { 185 | t.Errorf("span was not expected to be nil") 186 | } 187 | 188 | testFinish <- struct{}{} 189 | }() 190 | 191 | // Receive the message! 192 | err := r.Receive(ctx, m) 193 | if err != nil { 194 | t.Error(err) 195 | return 196 | } 197 | <-testFinish 198 | } 199 | 200 | // Tests that when a Receiver is wrapped by a Tracing Receiver, and 201 | // the message contains an invalid binary encodeded tracecontext, a span 202 | // is still sucessfully set 203 | func TestDecoder_SuccessfullySetsSpanWhenInvalidTraceContextBinary(t *testing.T) { 204 | testFinish := make(chan struct{}) 205 | msgChan := make(chan msgWithContext) 206 | r := Receiver(ChanReceiver{ 207 | c: msgChan, 208 | }) 209 | 210 | ctx, cancel := context.WithCancel(context.Background()) 211 | defer cancel() 212 | 213 | // Construct a message without base64 encoding 214 | m := &msg.Message{ 215 | Body: bytes.NewBufferString("abc123"), 216 | Attributes: msg.Attributes{}, 217 | } 218 | 219 | // "YWJjMTIz" is valid b64 220 | m.Attributes.Set("Tracecontext", "YWJjMTIz") 221 | 222 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 223 | go func() { 224 | result := <-msgChan 225 | expectedBody := "abc123" 226 | actual, _ := ioutil.ReadAll(result.msg.Body) 227 | if string(actual) != expectedBody { 228 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 229 | } 230 | 231 | span := trace.FromContext(result.ctx) 232 | if span == nil { 233 | t.Errorf("span was not expected to be nil") 234 | } 235 | 236 | testFinish <- struct{}{} 237 | }() 238 | 239 | // Receive the message! 240 | err := r.Receive(ctx, m) 241 | if err != nil { 242 | t.Error(err) 243 | return 244 | } 245 | <-testFinish 246 | } 247 | 248 | // Tests that when a Receiver is wrapped by TracingReceiver, and tracecontext 249 | // is present, along with a trace state a span is started and set in the 250 | // receive context with the correct parent context and state 251 | func TestDecoder_SuccessfullyDecodesSpanWhenTraceContextAndSpanIsPresent(t *testing.T) { 252 | testFinish := make(chan struct{}) 253 | msgChan := make(chan msgWithContext) 254 | r := Receiver(ChanReceiver{ 255 | c: msgChan, 256 | }) 257 | 258 | ctx, cancel := context.WithCancel(context.Background()) 259 | defer cancel() 260 | 261 | sc, b64Sc := makeSpanContext() 262 | 263 | // Construct a message with base64 encoding (YWJjMTIz == abc123) 264 | m := &msg.Message{ 265 | Body: bytes.NewBufferString("hello"), 266 | Attributes: msg.Attributes{}, 267 | } 268 | m.Attributes.Set("Tracecontext", b64Sc) 269 | m.Attributes.Set("Tracestate", "debug=true,log=false") 270 | 271 | // Wait for ChanReceiver to write the message to msgChan, assert on the body 272 | go func() { 273 | result := <-msgChan 274 | 275 | expectedBody := "hello" 276 | actual, _ := ioutil.ReadAll(result.msg.Body) 277 | if string(actual) != expectedBody { 278 | t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) 279 | } 280 | 281 | span := trace.FromContext(result.ctx) 282 | if span == nil { 283 | t.Errorf("span was not expected to be nil") 284 | } 285 | 286 | receivedSC := span.SpanContext() 287 | 288 | if receivedSC.TraceID != sc.TraceID { 289 | t.Errorf(cmp.Diff(receivedSC.TraceID, sc.TraceID)) 290 | } 291 | 292 | if receivedSC.Tracestate == nil { 293 | t.Errorf("tracestate was nil, expecting non-nil") 294 | } 295 | 296 | receivedTS := receivedSC.Tracestate 297 | 298 | entries := receivedTS.Entries() 299 | if len(entries) != 2 { 300 | t.Errorf("expected tracestate entries to be 2") 301 | } 302 | 303 | e1 := receivedTS.Entries()[0] 304 | if e1.Key != "debug" || e1.Value != "true" { 305 | t.Errorf("tracestate entry unexpected, got %s %s", e1.Key, e1.Value) 306 | } 307 | 308 | e2 := receivedTS.Entries()[1] 309 | if e2.Key != "log" || e2.Value != "false" { 310 | t.Errorf("tracestate entry unexpected, got %s %s", e2.Key, e2.Value) 311 | } 312 | 313 | testFinish <- struct{}{} 314 | }() 315 | 316 | // Receive the message! 317 | err := r.Receive(ctx, m) 318 | if err != nil { 319 | t.Error(err) 320 | return 321 | } 322 | <-testFinish 323 | } 324 | -------------------------------------------------------------------------------- /decorators/tracing/topic.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "fmt" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/zerofox-oss/go-msg" 13 | "go.opencensus.io/trace" 14 | "go.opencensus.io/trace/propagation" 15 | ) 16 | 17 | func msgAttributesToTrace(msgAttributes msg.Attributes) []trace.Attribute { 18 | traceAttributes := make([]trace.Attribute, len(msgAttributes)) 19 | for key, value := range msgAttributes { 20 | traceAttributes = append(traceAttributes, trace.StringAttribute(key, strings.Join(value, ";"))) 21 | } 22 | return traceAttributes 23 | } 24 | 25 | // Topic wraps a msg.Topic, attaching any tracing data 26 | // via msg.Attributes to send downstream 27 | func Topic(next msg.Topic, opts ...Option) msg.Topic { 28 | options := &Options{ 29 | SpanName: "msg.MessageWriter", 30 | StartOptions: trace.StartOptions{}, 31 | } 32 | 33 | for _, opt := range opts { 34 | opt(options) 35 | } 36 | 37 | return msg.TopicFunc(func(ctx context.Context) msg.MessageWriter { 38 | tracingCtx, span := trace.StartSpan( 39 | ctx, 40 | options.SpanName, 41 | trace.WithSampler(options.StartOptions.Sampler), 42 | ) 43 | 44 | return &tracingWriter{ 45 | Next: next.NewWriter(ctx), 46 | ctx: tracingCtx, 47 | onClose: span.End, 48 | options: options, 49 | } 50 | }) 51 | } 52 | 53 | type tracingWriter struct { 54 | Next msg.MessageWriter 55 | 56 | buf bytes.Buffer 57 | closed bool 58 | mux sync.Mutex 59 | ctx context.Context 60 | 61 | // onClose is used to end the span 62 | // and send data to tracing framework 63 | onClose func() 64 | 65 | options *Options 66 | } 67 | 68 | // Attributes returns the attributes associated with the MessageWriter. 69 | func (w *tracingWriter) Attributes() *msg.Attributes { 70 | return w.Next.Attributes() 71 | } 72 | 73 | func (w *tracingWriter) SetDelay(delay time.Duration) { 74 | w.Next.SetDelay(delay) 75 | } 76 | 77 | // Close adds tracing message attributes 78 | // writing to the next MessageWriter. 79 | func (w *tracingWriter) Close() error { 80 | w.mux.Lock() 81 | defer w.mux.Unlock() 82 | defer w.onClose() 83 | 84 | if w.closed { 85 | return msg.ErrClosedMessageWriter 86 | } 87 | w.closed = true 88 | 89 | dataToWrite := w.buf.Bytes() 90 | 91 | if span := trace.FromContext(w.ctx); span != nil { 92 | sc := span.SpanContext() 93 | bs := propagation.Binary(sc) 94 | traceBuf := make([]byte, base64.StdEncoding.EncodedLen(len(bs))) 95 | base64.StdEncoding.Encode(traceBuf, bs) 96 | 97 | attrs := *w.Attributes() 98 | 99 | span.AddAttributes(msgAttributesToTrace(attrs)...) 100 | 101 | attrs.Set(traceContextKey, string(traceBuf)) 102 | tracestateString := tracestateToString(sc) 103 | if tracestateString != "" { 104 | attrs.Set(traceStateKey, tracestateToString(sc)) 105 | } 106 | 107 | span.Annotate([]trace.Attribute{}, fmt.Sprintf("%q", dataToWrite)) 108 | 109 | } 110 | 111 | if _, err := w.Next.Write(dataToWrite); err != nil { 112 | return err 113 | } 114 | return w.Next.Close() 115 | } 116 | 117 | // Write writes bytes to an internal buffer. 118 | func (w *tracingWriter) Write(b []byte) (int, error) { 119 | w.mux.Lock() 120 | defer w.mux.Unlock() 121 | 122 | if w.closed { 123 | return 0, msg.ErrClosedMessageWriter 124 | } 125 | return w.buf.Write(b) 126 | } 127 | -------------------------------------------------------------------------------- /decorators/tracing/topic_test.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "testing" 7 | 8 | msg "github.com/zerofox-oss/go-msg" 9 | "github.com/zerofox-oss/go-msg/backends/mem" 10 | "go.opencensus.io/trace" 11 | "go.opencensus.io/trace/propagation" 12 | "go.opencensus.io/trace/tracestate" 13 | ) 14 | 15 | func TestTopic__SucessfullyInsertsTraceContext(t *testing.T) { 16 | c := make(chan *msg.Message, 2) 17 | 18 | // setup topics 19 | t1 := mem.Topic{C: c} 20 | t2 := Topic(&t1, WithSpanName("something.Different")) 21 | 22 | w := t2.NewWriter(context.Background()) 23 | w.Write([]byte("hello,")) 24 | w.Write([]byte("world!")) 25 | w.Close() 26 | 27 | m := <-c 28 | body, err := msg.DumpBody(m) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | expectedBody := "hello,world!" 34 | if string(body) != expectedBody { 35 | t.Fatalf("got %s expected %s", string(body), expectedBody) 36 | } 37 | 38 | tc := m.Attributes.Get("Tracecontext") 39 | if tc == "" { 40 | t.Fatalf("expected tracecontext attribute to be set") 41 | } 42 | 43 | b, err := base64.StdEncoding.DecodeString(tc) 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | 48 | _, ok := propagation.FromBinary(b) 49 | if !ok { 50 | t.Errorf("expected spanContext to be decoded from tracecontext attribute") 51 | } 52 | } 53 | 54 | func TestTopic__SucessfullyInsertsTraceContextWithTraceState(t *testing.T) { 55 | c := make(chan *msg.Message, 2) 56 | 57 | // setup topics 58 | t1 := mem.Topic{C: c} 59 | t2 := Topic(&t1, WithSpanName("something.Different")) 60 | 61 | pctx, pspan := trace.StartSpan(context.Background(), "PSpan") 62 | 63 | ts, err := tracestate.New(nil, tracestate.Entry{Key: "debug", Value: "true"}) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | 68 | psc := trace.SpanContext{ 69 | TraceID: pspan.SpanContext().TraceID, 70 | SpanID: pspan.SpanContext().SpanID, 71 | TraceOptions: pspan.SpanContext().TraceOptions, 72 | Tracestate: ts, 73 | } 74 | 75 | ctx, _ := trace.StartSpanWithRemoteParent(pctx, "SpanName", psc) 76 | 77 | w := t2.NewWriter(ctx) 78 | w.Write([]byte("hello,")) 79 | w.Write([]byte("world!")) 80 | w.Close() 81 | 82 | m := <-c 83 | body, err := msg.DumpBody(m) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | expectedBody := "hello,world!" 89 | if string(body) != expectedBody { 90 | t.Fatalf("got %s expected %s", string(body), expectedBody) 91 | } 92 | 93 | tc := m.Attributes.Get("Tracecontext") 94 | if tc == "" { 95 | t.Fatalf("expected tracecontext attribute to be set") 96 | } 97 | 98 | b, err := base64.StdEncoding.DecodeString(tc) 99 | if err != nil { 100 | t.Error(err) 101 | } 102 | 103 | _, ok := propagation.FromBinary(b) 104 | if !ok { 105 | t.Errorf("expected spanContext to be decoded from tracecontext attribute") 106 | } 107 | 108 | tsstring := m.Attributes.Get("Tracestate") 109 | if tsstring != "debug=true" { 110 | t.Fatalf("expected tracecontext attribute to be set") 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /decorators/tracing/utils.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "go.opencensus.io/trace" 8 | "go.opencensus.io/trace/tracestate" 9 | ) 10 | 11 | // CODE BASED ON: 12 | // https://github.com/census-instrumentation/opencensus-go/blob/ \ 13 | // master/plugin/ochttp/propagation/tracecontext/propagation.go 14 | 15 | const ( 16 | trimOWSRegexFmt = `^[\x09\x20]*(.*[^\x20\x09])[\x09\x20]*$` 17 | maxTracestateLen = 512 18 | ) 19 | 20 | var trimOWSRegExp = regexp.MustCompile(trimOWSRegexFmt) // nolint 21 | 22 | func tracestateToString(sc trace.SpanContext) string { 23 | pairs := make([]string, 0, len(sc.Tracestate.Entries())) 24 | if sc.Tracestate != nil { 25 | for _, entry := range sc.Tracestate.Entries() { 26 | pairs = append(pairs, strings.Join([]string{entry.Key, entry.Value}, "=")) 27 | } 28 | return strings.Join(pairs, ",") 29 | } 30 | return "" 31 | } 32 | 33 | func tracestateFromString(tracestateString string) *tracestate.Tracestate { 34 | var entries []tracestate.Entry // nolint 35 | pairs := strings.Split(tracestateString, ",") 36 | hdrLenWithoutOWS := len(pairs) - 1 // Number of commas 37 | for _, pair := range pairs { 38 | matches := trimOWSRegExp.FindStringSubmatch(pair) 39 | if matches == nil { 40 | return nil 41 | } 42 | pair = matches[1] 43 | hdrLenWithoutOWS += len(pair) 44 | if hdrLenWithoutOWS > maxTracestateLen { 45 | return nil 46 | } 47 | kv := strings.Split(pair, "=") 48 | if len(kv) != 2 { 49 | return nil 50 | } 51 | entries = append(entries, tracestate.Entry{Key: kv[0], Value: kv[1]}) 52 | } 53 | ts, err := tracestate.New(nil, entries...) 54 | if err != nil { 55 | return nil 56 | } 57 | 58 | return ts 59 | } 60 | -------------------------------------------------------------------------------- /docs/decisions/2024-07-08-multiserver.md: -------------------------------------------------------------------------------- 1 | --- 2 | Date: 2024/07/08 3 | Authors: @Xopherus, @shezadkhan137 4 | Status: Accepted 5 | --- 6 | 7 | # Multi-Server Fair Weighted Queues 8 | 9 | ## Context 10 | 11 | ZeroFox needs to prioritize messages within SQS queues, especially when services receive data from multiple sources with varying latency requirements. Prioritization is key to meeting SLAs for high-priority messages while ensuring lower-priority messages are not starved. Additionally, we need to dynamically allocate throughput based on message priority to ensure fairness and efficiency. 12 | 13 | ## Decisions 14 | 15 | 1. Implement an experimental Weighted Fair Queue algorithm as a decorator for the go-msg library (receiver). 16 | This decorator, called `WeightedFairReceiver`, will enable message prioritization based on assigned weights. 17 | 18 | 2. Implement a `MultiServer` wrapper around `WeightedFairReceiver`. 19 | This server will consume messages from multiple underlying servers into a single receiver, guaranteeing consumption in proportion to assigned weights. 20 | -------------------------------------------------------------------------------- /docs/decisions/2024-07-09-experimental-changes.md: -------------------------------------------------------------------------------- 1 | --- 2 | Date: 2024/07/09 3 | Authors: @Xopherus, @shezadkhan137 4 | Status: Accepted 5 | --- 6 | 7 | # Experimental Libraries 8 | 9 | ## Context 10 | 11 | Go-msg is an open source library, although primarily used internally (at ZeroFox). 12 | There are new features and changes we wish to implement, 13 | although it is sometimes difficult to merge because we have 14 | a higher standard for excellence (given the code is open source). 15 | 16 | It would be useful to have a mechanism to indicate that changes we introduce can be experimental (not production ready), 17 | and should be used at your own risk. 18 | This would allow us to innovate with additional backends, decorators, and primitives 19 | without changing the core of the library 20 | until we are confident that such changes should be accepted. 21 | 22 | ## Decision 23 | 24 | We will create an `go-msg/x/` directory which will house experimental features. 25 | We chose this model after [Go's experimental libraries](https://pkg.go.dev/golang.org/x/exp#section-readme). 26 | 27 | As a caveat, packages here are **experimental and unreliable**. They may be promoted to the main package, 28 | modified entirely, or removed. 29 | 30 | ## Consequences 31 | 32 | Every experimental package introduced should be accompanied with a Decision Record, 33 | indicating what we're adding and why. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zerofox-oss/go-msg 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/JimWen/gods-generic v0.10.2 7 | github.com/asecurityteam/rolling v2.0.4+incompatible 8 | github.com/google/go-cmp v0.6.0 9 | github.com/pierrec/lz4/v4 v4.1.8 10 | github.com/stretchr/testify v1.8.4 11 | go.opencensus.io v0.24.0 12 | go.opentelemetry.io/otel v1.24.0 13 | go.opentelemetry.io/otel/bridge/opencensus v1.24.0 14 | go.opentelemetry.io/otel/sdk v1.24.0 15 | go.opentelemetry.io/otel/trace v1.24.0 16 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 17 | pgregory.net/rapid v1.1.0 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/go-logr/logr v1.4.2 // indirect 23 | github.com/go-logr/stdr v1.2.2 // indirect 24 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 25 | github.com/kr/text v0.2.0 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 28 | go.opentelemetry.io/otel/sdk/metric v1.24.0 // indirect 29 | golang.org/x/sys v0.20.0 // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/JimWen/gods-generic v0.10.2 h1:ib/BF6W5+ANQJinlNxHYETH1BtxZASkBOV3v4mHSYYY= 4 | github.com/JimWen/gods-generic v0.10.2/go.mod h1:ukDWk4Hb0hovQbhqitDTeOK4Hz+IK0y3q5QKQdri3as= 5 | github.com/asecurityteam/rolling v2.0.4+incompatible h1:WOSeokINZT0IDzYGc5BVcjLlR9vPol08RvI2GAsmB0s= 6 | github.com/asecurityteam/rolling v2.0.4+incompatible/go.mod h1:2D4ba5ZfYCWrIMleUgTvc8pmLExEuvu3PDwl+vnG58Q= 7 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 8 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 9 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 10 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 15 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 16 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 17 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 18 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 19 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 20 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 21 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 22 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 23 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 24 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 25 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 26 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 27 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 31 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 32 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 33 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 34 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 35 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 36 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 37 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 38 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 39 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 40 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 42 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 44 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 45 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 46 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 47 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 48 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 49 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 50 | github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4= 51 | github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 55 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 56 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 57 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 58 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 59 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 60 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 61 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 62 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 63 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 64 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 65 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 66 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 67 | go.opentelemetry.io/otel/bridge/opencensus v1.24.0 h1:Vlhy5ee5k5R0zASpH+9AgHiJH7xnKACI3XopO1tUZfY= 68 | go.opentelemetry.io/otel/bridge/opencensus v1.24.0/go.mod h1:jRjVXV/X38jyrnHtvMGN8+9cejZB21JvXAAvooF2s+Q= 69 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 70 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 71 | go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= 72 | go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= 73 | go.opentelemetry.io/otel/sdk/metric v1.24.0 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8= 74 | go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0= 75 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 76 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 77 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 78 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 79 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 80 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 81 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 82 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 83 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 84 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 85 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 86 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 87 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 88 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 89 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 90 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 91 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 92 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 93 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 95 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 96 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 99 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 100 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 101 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 102 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 103 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 104 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 105 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 106 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 107 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 108 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 109 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 110 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 111 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 112 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 113 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 114 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 115 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 116 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 117 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 118 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 119 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 120 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 121 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 122 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 123 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 124 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 125 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 126 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 127 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 128 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 129 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 130 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 131 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 132 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 133 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 134 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 135 | pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= 136 | pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= 137 | -------------------------------------------------------------------------------- /msg.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "io" 8 | "net/textproto" 9 | "time" 10 | ) 11 | 12 | // Attributes represent the key-value metadata for a Message. 13 | type Attributes map[string][]string 14 | 15 | // from https://golang.org/src/net/http/header.go#L62 16 | func (a Attributes) clone() Attributes { 17 | a2 := make(Attributes, len(a)) 18 | for k, vv := range a { 19 | vv2 := make([]string, len(vv)) 20 | copy(vv2, vv) 21 | a2[k] = vv2 22 | } 23 | return a2 24 | } 25 | 26 | // Get returns the first value associated with the given key. 27 | // It is case insensitive; CanonicalMIME is used to cannonicalize the provided 28 | // key. If there are no values associated with the key, Get returns "". 29 | // To access multiple values of a key, or to use non-canonical keys, 30 | // access the map directly. 31 | func (a Attributes) Get(key string) string { 32 | return textproto.MIMEHeader(a).Get(key) 33 | } 34 | 35 | // Set sets the header entries associated with key the single element 36 | // element value. It replaces any existing values associated with key. 37 | // 38 | // Note: MIMEHeader automatically capitalizes the first letter of the key. 39 | func (a Attributes) Set(key, value string) { 40 | textproto.MIMEHeader(a).Set(key, value) 41 | } 42 | 43 | // A Message represents a discrete message in a messaging system. 44 | type Message struct { 45 | Attributes Attributes 46 | Body io.Reader 47 | } 48 | 49 | // WithBody creates a new Message with the given io.Reader as a Body 50 | // containing the parent's Attributes. 51 | // 52 | // p := &Message{ 53 | // Attributes: Attributes{}, 54 | // Body: strings.NewReader("hello world"), 55 | // } 56 | // p.Attributes.Set("hello", "world") 57 | // m := WithBody(p, strings.NewReader("world hello") 58 | func WithBody(parent *Message, r io.Reader) *Message { 59 | return &Message{ 60 | Attributes: parent.Attributes.clone(), 61 | Body: r, 62 | } 63 | } 64 | 65 | // DumpBody returns the contents of m.Body 66 | // while resetting m.Body 67 | // allowing it to be read from later. 68 | func DumpBody(m *Message) ([]byte, error) { 69 | b := m.Body 70 | // inspired by https://golang.org/src/net/http/httputil/dump.go#L26 71 | var buf bytes.Buffer 72 | 73 | if _, err := buf.ReadFrom(b); err != nil { 74 | return nil, err 75 | } 76 | m.Body = &buf 77 | 78 | return buf.Bytes(), nil 79 | } 80 | 81 | // CloneBody returns a reader 82 | // with the same contents and m.Body. 83 | // m.Body is reset allowing it to be read from later. 84 | func CloneBody(m *Message) (io.Reader, error) { 85 | b, err := DumpBody(m) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return bytes.NewBuffer(b), nil 91 | } 92 | 93 | // A Receiver processes a Message. 94 | // 95 | // Receive should process the message and then return. Returning signals that 96 | // the message has been processed. It is not valid to read from the Message.Body 97 | // after or concurrently with the completion of the Receive call. 98 | // 99 | // If Receive returns an error, the server (the caller of Receive) assumes the 100 | // message has not been processed and, depending on the underlying pub/sub 101 | // system, the message should be put back on the message queue. 102 | type Receiver interface { 103 | Receive(context.Context, *Message) error 104 | } 105 | 106 | // The ReceiverFunc is an adapter to allow the use of ordinary functions 107 | // as a Receiver. ReceiverFunc(f) is a Receiver that calls f. 108 | type ReceiverFunc func(context.Context, *Message) error 109 | 110 | // Receive calls f(ctx,m) 111 | func (f ReceiverFunc) Receive(ctx context.Context, m *Message) error { 112 | return f(ctx, m) 113 | } 114 | 115 | // ErrServerClosed represents a completed Shutdown 116 | var ErrServerClosed = errors.New("msg: server closed") 117 | 118 | // A Server serves messages to a receiver. 119 | type Server interface { 120 | // Serve is a blocking function that gets data from an input stream, 121 | // creates a message, and calls Receive() on the provided receiver 122 | // with the Message and a Context derived from context.Background(). 123 | // For example: 124 | // 125 | // parentctx = context.WithCancel(context.Background()) 126 | // err := r.Receive(parentctx, m) 127 | // 128 | // Serve will return ErrServerClosed after Shutdown completes. Additional 129 | // error types should be considered to represent error conditions unique 130 | // to the implementation of a specific technology. 131 | // 132 | // Serve() should continue to listen until Shutdown is called on 133 | // the Server. 134 | Serve(Receiver) error 135 | 136 | // Shutdown gracefully shuts down the Server by letting any messages in 137 | // flight finish processing. If the provided context cancels before 138 | // shutdown is complete, the Context's error is returned. 139 | Shutdown(context.Context) error 140 | } 141 | 142 | // ErrClosedMessageWriter is the error used for write or close operations on a closed MessageWriter. 143 | var ErrClosedMessageWriter = errors.New("msg: MessageWriter closed") 144 | 145 | // A MessageWriter interface is used to write a message to 146 | // an underlying data stream. 147 | type MessageWriter interface { 148 | io.Writer 149 | // Close should be called to signify the completion of a Write. Attributes 150 | // that represent a transform applied to a message should also be written 151 | // at this time. 152 | // 153 | // Close should forward a message to another MessageWriter or persist 154 | // to the messaging system. 155 | // 156 | // Once Close has been called, all subsequent Write and Close calls will result 157 | // in an ErrClosedMessageWriter error. 158 | io.Closer 159 | Attributes() *Attributes 160 | // SetDelay sets a duration to delay the message delivery by. 161 | // The delay is relative to the time when the message is persisted to the messaging system. 162 | // A zero or negative duration means no delay. 163 | SetDelay(delay time.Duration) 164 | } 165 | 166 | // Topic is a generic interface where messages are sent in a messaging system. 167 | // 168 | // Multiple goroutines may invoke method on a Topic simultaneously. 169 | type Topic interface { 170 | // NewWriter returns a new MessageWriter 171 | NewWriter(context.Context) MessageWriter 172 | } 173 | 174 | // The TopicFunc is an adapter to allow the use of ordinary functions 175 | // as a Topic. TopicFunc(f) is a Topic that calls f. 176 | type TopicFunc func(context.Context) MessageWriter 177 | 178 | // Ensure TopicFunc implements Topic 179 | var _ Topic = TopicFunc(nil) 180 | 181 | // NewWriter calls f(ctx,m) 182 | func (t TopicFunc) NewWriter(ctx context.Context) MessageWriter { 183 | return t(ctx) 184 | } 185 | -------------------------------------------------------------------------------- /msg_test.go: -------------------------------------------------------------------------------- 1 | package msg_test 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "net/textproto" 7 | "os" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | msg "github.com/zerofox-oss/go-msg" 14 | ) 15 | 16 | const expected = "hello world" 17 | 18 | func TestMain(m *testing.M) { 19 | os.Exit(m.Run()) 20 | } 21 | 22 | func TestGetAttribute(t *testing.T) { 23 | a := msg.Attributes{} 24 | 25 | // doesn't return if nothing is there 26 | if v := a.Get("foo"); v != "" { 27 | t.Errorf("expected nothing, got %s", v) 28 | } 29 | 30 | // returns if something is there 31 | a.Set("foo", "bar") 32 | if v := a.Get("foo"); v != "bar" { 33 | t.Errorf("expected bar, got %s", v) 34 | } 35 | 36 | // returns if something is there (case is different) 37 | if v := a.Get("FOO"); v != "bar" { 38 | t.Errorf("expected bar, got %s", v) 39 | } 40 | } 41 | 42 | func TestSetAttribute(t *testing.T) { 43 | a := msg.Attributes{} 44 | 45 | // if k/v not set, set it 46 | k := textproto.CanonicalMIMEHeaderKey("foo") 47 | 48 | a.Set("foo", "bar") 49 | if v := a[k]; !reflect.DeepEqual(v, []string{"bar"}) { 50 | t.Errorf("expected bar, got %s", v) 51 | } 52 | 53 | // if same key, override value 54 | a.Set("foo", "baz") 55 | if v := a[k]; !reflect.DeepEqual(v, []string{"baz"}) { 56 | t.Errorf("expected baz, got %s", v) 57 | } 58 | 59 | // if same key (different case), override value 60 | k = textproto.CanonicalMIMEHeaderKey("FOO") 61 | 62 | a.Set("FOO", "bin") 63 | if v := a[k]; !reflect.DeepEqual(v, []string{"bin"}) { 64 | t.Errorf("expected bin, got %s", v) 65 | } 66 | } 67 | 68 | func TestDumpBody(t *testing.T) { 69 | m := &msg.Message{ 70 | Body: strings.NewReader(expected), 71 | } 72 | b, err := msg.DumpBody(m) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | if string(b) != expected { 77 | t.Errorf("Dumped body does not match expected: %s != %s", expected, string(b)) 78 | } 79 | } 80 | 81 | func TestCloneBody(t *testing.T) { 82 | m := &msg.Message{ 83 | Body: strings.NewReader(expected), 84 | } 85 | b, err := msg.CloneBody(m) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | bb, err := ioutil.ReadAll(b) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | if string(bb) != expected { 94 | t.Errorf("Cloned body does not match expected: %s != %s", expected, string(bb)) 95 | } 96 | } 97 | 98 | func TestWithBody(t *testing.T) { 99 | m := &msg.Message{ 100 | Attributes: msg.Attributes{}, 101 | Body: strings.NewReader("hello world"), 102 | } 103 | m.Attributes.Set("foo", "bar") 104 | 105 | mm := msg.WithBody(m, strings.NewReader("hello new world")) 106 | body, err := msg.DumpBody(mm) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | 111 | // assert attributes are copied but body is new 112 | if mm.Attributes.Get("foo") != "bar" { 113 | t.Errorf("Attributes failed to copy") 114 | } 115 | if string(body) != "hello new world" { 116 | t.Errorf("body does not match expected %s", string(body)) 117 | } 118 | 119 | // assert that message attributes are not shared 120 | m.Attributes.Set("test", "one") 121 | mm.Attributes.Set("test", "two") 122 | 123 | if m.Attributes.Get("test") == mm.Attributes.Get("test") { 124 | t.Errorf("message attributes should not be the same") 125 | } 126 | } 127 | 128 | type testMessageWriter struct { 129 | attrs msg.Attributes 130 | delay time.Duration 131 | } 132 | 133 | func (t *testMessageWriter) Write(p []byte) (n int, err error) { 134 | return len(p), nil 135 | } 136 | 137 | func (t *testMessageWriter) Close() error { 138 | return nil 139 | } 140 | 141 | func (t *testMessageWriter) Attributes() *msg.Attributes { 142 | return &t.attrs 143 | } 144 | 145 | func (t *testMessageWriter) SetDelay(d time.Duration) { 146 | t.delay = d 147 | } 148 | 149 | func TestTopicFuncNewWriter(t *testing.T) { 150 | w := &testMessageWriter{} 151 | tf := msg.TopicFunc(func(ctx context.Context) msg.MessageWriter { 152 | return w 153 | }) 154 | 155 | mw := tf.NewWriter(context.Background()) 156 | if mw != w { 157 | t.Error("TopicFunc did not return expected MessageWriter") 158 | } 159 | 160 | expectedDelay := 5 * time.Second 161 | mw.SetDelay(expectedDelay) 162 | if w.delay != expectedDelay { 163 | t.Errorf("SetDelay(%v) = %v, want %v", expectedDelay, w.delay, expectedDelay) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /x/README.md: -------------------------------------------------------------------------------- 1 | # Experimental Packages (x) 2 | 3 | This directory holds experimental packages ([decision record](../docs/decisions/2024-07-09-experimental-changes)). 4 | The intent is to introduce new features which may or may not become a part of the core library. 5 | 6 | Code in this directory may not be backwards compatible. 7 | -------------------------------------------------------------------------------- /x/multiserver/README.md: -------------------------------------------------------------------------------- 1 | # Multiserver 2 | 3 | The `multiserver` package provides a `MultiServer` that can serve messages from multiple underlying servers to a single receiver. The server will consume messages from the underlying servers in the ratio of the weights provided. 4 | 5 | ### Example 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "bytes" 12 | "context" 13 | "fmt" 14 | "time" 15 | 16 | "github.com/zerofox-oss/go-msg" 17 | "github.com/zerofox-oss/go-msg/backends/mem" 18 | "github.com/zerofox-oss/go-msg/x/multiserver" 19 | ) 20 | 21 | func main() { 22 | // Create memory servers 23 | server1 := mem.NewServer(make(chan *msg.Message, 100), 10) 24 | server2 := mem.NewServer(make(chan *msg.Message, 100), 10) 25 | 26 | // Define server weights 27 | serverWeights := []multiserver.ServerWeight{ 28 | {Server: server1, Weight: 1.0}, 29 | {Server: server2, Weight: 2.0}, 30 | } 31 | 32 | // Create MultiServer 33 | mserver, err := multiserver.NewMultiServer(10, serverWeights) 34 | if err != nil { 35 | fmt.Println("Error creating MultiServer:", err) 36 | return 37 | } 38 | 39 | // Start serving messages 40 | go func() { 41 | mserver.Serve(msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { 42 | fmt.Println("Received message:", m) 43 | return nil 44 | })) 45 | }() 46 | 47 | // Simulate sending messages 48 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 49 | defer cancel() 50 | 51 | go func() { 52 | for { 53 | select { 54 | case <-ctx.Done(): 55 | return 56 | case server1.C <- &msg.Message{Body: bytes.NewBuffer([]byte("message from server1"))}: 57 | case server2.C <- &msg.Message{Body: bytes.NewBuffer([]byte("message from server2"))}: 58 | } 59 | } 60 | }() 61 | 62 | <-ctx.Done() 63 | mserver.Shutdown(context.Background()) 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /x/multiserver/multiserver.go: -------------------------------------------------------------------------------- 1 | package multiserver 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | msg "github.com/zerofox-oss/go-msg" 9 | "golang.org/x/sync/errgroup" 10 | ) 11 | 12 | // MultiServer is a server that can serve messages from multiple underlying servers 13 | // to as single receiver. The server will consume messages from the underlying servers 14 | // in the ratio of the weights provided. 15 | type MultiServer struct { 16 | servers []msg.Server 17 | weights []float64 18 | concurrency int 19 | queueWaitTime time.Duration 20 | wfr *WeightedFairReceiver 21 | } 22 | 23 | type ServerWeight struct { 24 | Server msg.Server 25 | Weight float64 26 | } 27 | 28 | // MultiServerOption is a functional option for the MultiServer. 29 | type MultiServerOption func(*MultiServer) 30 | 31 | // WithQueueWaitTime sets the time to wait for a message to arrive. 32 | func WithQueueWaitTime(queueWaitTime time.Duration) MultiServerOption { 33 | return func(m *MultiServer) { 34 | m.queueWaitTime = queueWaitTime 35 | } 36 | } 37 | 38 | // NewMultiServer creates a new MultiServer with the given concurrency. 39 | // The server will distribute the messages to underlying receiver from 40 | // the given servers in the ratio of the weights provided. 41 | func NewMultiServer(concurrency int, serverWeights []ServerWeight, opts ...MultiServerOption) (*MultiServer, error) { 42 | if len(serverWeights) == 0 { 43 | return nil, errors.New("serverWeights must not be empty") 44 | } 45 | 46 | if concurrency <= 0 { 47 | return nil, errors.New("concurrency must be greater than 0") 48 | } 49 | 50 | servers := make([]msg.Server, 0, len(serverWeights)) 51 | weights := make([]float64, 0, len(serverWeights)) 52 | 53 | for _, s := range serverWeights { 54 | servers = append(servers, s.Server) 55 | weights = append(weights, s.Weight) 56 | } 57 | 58 | server := &MultiServer{ 59 | concurrency: concurrency, 60 | servers: servers, 61 | weights: weights, 62 | queueWaitTime: 1 * time.Millisecond, 63 | } 64 | 65 | for _, opt := range opts { 66 | opt(server) 67 | } 68 | 69 | return server, nil 70 | } 71 | 72 | // Serve serves messages to the underlying servers. 73 | func (m *MultiServer) Serve(msg msg.Receiver) error { 74 | m.wfr = NewWeightedFairReceiver( 75 | m.weights, 76 | m.concurrency, 77 | m.queueWaitTime, 78 | msg, 79 | ) 80 | 81 | g := errgroup.Group{} 82 | for i, s := range m.servers { 83 | s := s 84 | i := i 85 | g.Go(func() error { 86 | return s.Serve(m.wfr.WithPriorityReceiver(i)) 87 | }) 88 | } 89 | 90 | return g.Wait() 91 | } 92 | 93 | // Shutdown shuts down the server. 94 | func (m *MultiServer) Shutdown(ctx context.Context) error { 95 | g := errgroup.Group{} 96 | for _, s := range m.servers { 97 | s := s 98 | g.Go(func() error { 99 | return s.Shutdown(ctx) 100 | }) 101 | } 102 | err := g.Wait() 103 | if err != nil { 104 | return err 105 | } 106 | 107 | return m.wfr.Close(ctx) 108 | } 109 | -------------------------------------------------------------------------------- /x/multiserver/multiserver_test.go: -------------------------------------------------------------------------------- 1 | package multiserver_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "math/rand" 8 | "strconv" 9 | "sync/atomic" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/assert" 14 | "github.com/zerofox-oss/go-msg" 15 | "github.com/zerofox-oss/go-msg/backends/mem" 16 | "github.com/zerofox-oss/go-msg/x/multiserver" 17 | "pgregory.net/rapid" 18 | ) 19 | 20 | func sendMessages(ctx context.Context, inputChan chan *msg.Message) { 21 | for { 22 | select { 23 | case <-ctx.Done(): 24 | return 25 | case inputChan <- &msg.Message{ 26 | Body: bytes.NewBuffer([]byte("hello world")), 27 | Attributes: msg.Attributes{}, 28 | }: 29 | } 30 | } 31 | } 32 | 33 | func TestMultiServer(t *testing.T) { 34 | t.Parallel() 35 | 36 | for i := 0; i <= 2; i++ { 37 | t.Run(fmt.Sprintf("TestMultiServer_%d", i), func(t *testing.T) { 38 | t.Parallel() 39 | 40 | rapid.Check(t, func(t *rapid.T) { 41 | numServers := rapid.IntRange(1, 5).Draw(t, "numServers") 42 | 43 | serverConcurrency := 5 44 | inputChanBuffer := 50 45 | 46 | counts := make([]atomic.Int32, numServers) 47 | inputChans := make([]chan *msg.Message, numServers) 48 | serverWeights := make([]multiserver.ServerWeight, numServers) 49 | 50 | for i := 0; i < numServers; i++ { 51 | inputChan := make(chan *msg.Message, inputChanBuffer) 52 | server := mem.NewServer(inputChan, serverConcurrency) 53 | weight := rapid.IntRange(1, 10).Draw(t, "weight") 54 | serverWeights[i] = multiserver.ServerWeight{ 55 | Server: server, 56 | Weight: float64(weight), 57 | } 58 | inputChans[i] = inputChan 59 | } 60 | 61 | mserver, err := multiserver.NewMultiServer(serverConcurrency, serverWeights) 62 | assert.NoError(t, err) 63 | 64 | go func() { 65 | mserver.Serve(msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { 66 | msgPriority := m.Attributes.Get(multiserver.MultiServerMsgPriority) 67 | p, err := strconv.Atoi(msgPriority) 68 | if err != nil { 69 | assert.Fail(t, "failed to parse priority") 70 | } 71 | counts[p].Add(1) 72 | time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) 73 | return nil 74 | })) 75 | }() 76 | 77 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 78 | defer cancel() 79 | 80 | for i := 0; i < numServers; i++ { 81 | go sendMessages(ctx, inputChans[i]) 82 | } 83 | 84 | <-ctx.Done() 85 | mserver.Shutdown(context.Background()) 86 | 87 | totalWeight := 0 88 | totalCounts := 0 89 | for i := 0; i < numServers; i++ { 90 | totalWeight += int(serverWeights[i].Weight) 91 | totalCounts += int(counts[i].Load()) 92 | } 93 | 94 | delta := float64(totalCounts) * float64(0.1) 95 | 96 | t.Logf("Total weight: %d\n", totalWeight) 97 | t.Logf("Total counts: %d\n", totalCounts) 98 | t.Logf("Total counts delta: %f\n", delta) 99 | 100 | for i := 0; i < numServers; i++ { 101 | weight := int(serverWeights[i].Weight) 102 | expectedCount := (weight * totalCounts) / totalWeight 103 | serverCount := int(counts[i].Load()) 104 | t.Logf("Server %d: weight %d, expected count %d, actual count %d\n", i, weight, expectedCount, serverCount) 105 | assert.InDeltaf( 106 | t, expectedCount, serverCount, delta, 107 | "Server %d: weight %d, expected count %d, actual count %d\n", i, weight, expectedCount, serverCount) 108 | } 109 | }) 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /x/multiserver/receiver.go: -------------------------------------------------------------------------------- 1 | package multiserver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "time" 8 | 9 | pq "github.com/JimWen/gods-generic/queues/priorityqueue" 10 | "github.com/JimWen/gods-generic/utils" 11 | "github.com/asecurityteam/rolling" 12 | msg "github.com/zerofox-oss/go-msg" 13 | ) 14 | 15 | // MultiServerMsgPriority is the key used to store the priority of a message in the message attributes. 16 | const MultiServerMsgPriority = "x-multiserver-priority" 17 | 18 | type weightedMessage struct { 19 | msg *msg.Message 20 | context context.Context 21 | vFinish float64 22 | priority int 23 | doneChan chan error 24 | } 25 | 26 | // WeightedFairReceiver implements a "fair" receiver that processes messages based on their priority. 27 | // See https://en.wikipedia.org/wiki/Fair_queuing for more information, about fairness. 28 | // The concrete implementation is based on the Weighted Fair Queuing algorithm. 29 | // At a high level the algorithm works as follows: 30 | // 1. Each message is assigned a virtual finish time. The virtual finish time is defined as the 31 | // time of the last message processed for that priority plus a weighting factor. The higher the 32 | // the weight the smaller the virtual finish time is for that message. 33 | // 2. The message is then enqueued in a priority queue based on the virtual finish time. 34 | // 3. Every "tick" we select the message with the smallest virtual finish time and process it. 35 | type WeightedFairReceiver struct { 36 | // underlying receiver 37 | receiver msg.Receiver 38 | 39 | // time to wait for a message to arrive 40 | // the longer you wait, the better the fairness 41 | // the shorter the better the latency. 42 | queueWaitTime time.Duration 43 | 44 | // weights for each priority level 45 | // the higher the weight, the more messages 46 | // will be processed for that priority 47 | weights []float64 48 | 49 | // max number of concurrent messages that can 50 | // be processed at the same time by the receiver 51 | maxConcurrent int 52 | 53 | // a rough estimate time to process a message 54 | // this should be in milliseconds. 55 | initialEstimatedCost float64 56 | 57 | receiveChan chan *weightedMessage 58 | queue *pq.Queue[*weightedMessage] 59 | startTime int 60 | lastVFinish []float64 61 | 62 | timePolicy *rolling.TimePolicy 63 | closeChan chan chan error 64 | } 65 | 66 | // NewWeightedFairReceiver creates a new WeightedFairReceiver. 67 | // The receiver will process messages based on their priority level. 68 | func NewWeightedFairReceiver( 69 | weights []float64, 70 | maxConcurrent int, 71 | queueWaitTime time.Duration, 72 | receiver msg.Receiver, 73 | ) *WeightedFairReceiver { 74 | priorityQueue := pq.NewWith(func(a, b *weightedMessage) int { 75 | return utils.NumberComparator(a.vFinish, b.vFinish) 76 | }) 77 | 78 | wfr := &WeightedFairReceiver{ 79 | weights: weights, 80 | lastVFinish: make([]float64, len(weights)), 81 | receiver: receiver, 82 | queue: priorityQueue, 83 | startTime: 1, 84 | queueWaitTime: queueWaitTime, 85 | maxConcurrent: maxConcurrent, 86 | receiveChan: make(chan *weightedMessage), 87 | timePolicy: rolling.NewTimePolicy(rolling.NewWindow(10000), 1*time.Millisecond), 88 | initialEstimatedCost: 100.0, 89 | closeChan: make(chan chan error), 90 | } 91 | 92 | go wfr.dispatch() 93 | return wfr 94 | } 95 | 96 | // WithPriorityReceiver returns a new msg.Receiver that should be used to receive messages 97 | // at a specific priority level. 98 | func (w *WeightedFairReceiver) WithPriorityReceiver(priority int) msg.Receiver { 99 | return msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { 100 | return w.Receive(ctx, m, priority) 101 | }) 102 | } 103 | 104 | // Receive receives a message with a specific priority level. 105 | func (w *WeightedFairReceiver) Receive(ctx context.Context, m *msg.Message, priority int) error { 106 | wm := &weightedMessage{ 107 | msg: m, 108 | priority: priority, 109 | doneChan: make(chan error, 1), 110 | context: ctx, 111 | } 112 | w.receiveChan <- wm 113 | return <-wm.doneChan 114 | } 115 | 116 | // Close closes the receiver. 117 | func (w *WeightedFairReceiver) Close(ctx context.Context) error { 118 | doneChan := make(chan error) 119 | w.closeChan <- doneChan 120 | select { 121 | case <-ctx.Done(): 122 | return ctx.Err() 123 | case err := <-doneChan: 124 | return err 125 | } 126 | } 127 | 128 | func (w *WeightedFairReceiver) estimateCost() float64 { 129 | return w.timePolicy.Reduce(rolling.Avg) 130 | } 131 | 132 | func (w *WeightedFairReceiver) dispatch() { 133 | maxConcurrentReceives := make(chan struct{}, w.maxConcurrent) 134 | timer := time.NewTicker(w.queueWaitTime) 135 | 136 | doReceive := func() { 137 | select { 138 | case maxConcurrentReceives <- struct{}{}: 139 | if mw, ok := w.queue.Dequeue(); ok { 140 | go func(wm weightedMessage) { 141 | defer func() { 142 | <-maxConcurrentReceives 143 | }() 144 | st := time.Now() 145 | wm.msg.Attributes.Set(MultiServerMsgPriority, fmt.Sprint(wm.priority)) 146 | result := w.receiver.Receive(wm.context, wm.msg) 147 | w.timePolicy.Append(float64(time.Since(st).Milliseconds())) 148 | wm.doneChan <- result 149 | }(*mw) 150 | } else { 151 | <-maxConcurrentReceives 152 | } 153 | default: 154 | } 155 | } 156 | 157 | for { 158 | select { 159 | case doneChan := <-w.closeChan: 160 | doneChan <- nil 161 | // TODO: process remaining messages 162 | // that are queued in order to cleanly shutdown 163 | return 164 | case wm := <-w.receiveChan: 165 | vStart := math.Max(float64(w.startTime), w.lastVFinish[wm.priority]) 166 | estimatedCost := w.estimateCost() 167 | if math.IsNaN(estimatedCost) { 168 | estimatedCost = w.initialEstimatedCost 169 | } 170 | estimatedCost = math.Round(estimatedCost) 171 | weight2 := float64(estimatedCost / w.weights[wm.priority]) 172 | vFinish := vStart + weight2 173 | wm.vFinish = vFinish 174 | w.lastVFinish[wm.priority] = vFinish 175 | w.queue.Enqueue(wm) 176 | case <-timer.C: 177 | doReceive() 178 | } 179 | } 180 | } 181 | --------------------------------------------------------------------------------