├── .errignore ├── .github └── CONTRIBUTING.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── codecov.yml ├── glide.lock ├── glide.yaml └── src ├── internal ├── attributes │ ├── catalog.go │ ├── catalog_matcher.go │ ├── catalog_test.go │ ├── collection.go │ ├── collection_test.go │ ├── diff.go │ ├── diff_test.go │ ├── ginkgo_test.go │ ├── list.go │ ├── list_test.go │ ├── table.go │ ├── table_test.go │ ├── vattr.go │ ├── vlist.go │ ├── vlist_test.go │ ├── vtable.go │ └── vtable_test.go ├── command │ ├── invoker.go │ ├── response.go │ └── server.go ├── functest │ ├── canned_responses.go │ ├── must.go │ ├── namespaces.go │ ├── peer.go │ └── pkg.go ├── localsession │ ├── revision.go │ ├── revision_logging.go │ ├── session.go │ ├── session_logging.go │ ├── session_state.go │ └── store.go ├── namespaces │ ├── ginkgo_test.go │ ├── validate.go │ └── validate_test.go ├── notify │ ├── listener.go │ └── notifier.go ├── opentr │ ├── command.go │ ├── command_test.go │ ├── common.go │ ├── common_test.go │ ├── ginkgo_test.go │ ├── lazy_string.go │ ├── mockspan_test.go │ ├── notify.go │ ├── pkg.go │ ├── session.go │ ├── session_test.go │ └── span.go ├── remotesession │ ├── cache.go │ ├── client.go │ ├── client_logging.go │ ├── ginkgo_test.go │ ├── logging.go │ ├── revision.go │ ├── revision_functional_test.go │ ├── server.go │ ├── server_logging.go │ ├── session.go │ ├── store.go │ └── types.go ├── revisions │ ├── closed.go │ └── store.go ├── service │ ├── error.go │ ├── service.go │ └── statemachine.go └── x │ ├── bufferpool │ ├── bufferpool.go │ ├── bufferpool_test.go │ └── ginkgo_test.go │ ├── cbor │ ├── cbor.go │ ├── cbor_test.go │ └── ginkgo_test.go │ ├── env │ └── env.go │ ├── repr │ ├── escape.go │ ├── escape_test.go │ ├── ginkgo_test.go │ └── pkg.go │ └── syncx │ ├── lock.go │ └── pkg.go ├── rinq ├── attr.go ├── attr_example_test.go ├── attr_test.go ├── command.go ├── command_example_test.go ├── command_test.go ├── constraint │ ├── constraint.go │ ├── constraint_test.go │ ├── ginkgo_test.go │ ├── pkg.go │ ├── stringer.go │ ├── validator.go │ └── visitor.go ├── ginkgo_test.go ├── ident │ ├── ginkgo_test.go │ ├── message_id.go │ ├── message_id_test.go │ ├── peer_id.go │ ├── peer_id_test.go │ ├── pkg.go │ ├── ref.go │ ├── ref_test.go │ ├── session_id.go │ ├── session_id_test.go │ ├── validate.go │ └── validate_test.go ├── notification.go ├── options │ ├── env.go │ ├── env_test.go │ ├── gingo_test.go │ ├── option.go │ ├── options.go │ ├── options_test.go │ ├── pkg.go │ └── visitor.go ├── payload.go ├── payload_test.go ├── peer.go ├── peer_example_test.go ├── pkg.go ├── pkg_math_example_test.go ├── revision.go ├── revision_example_test.go ├── revision_test.go ├── session.go ├── session_example_test.go ├── session_test.go └── trace │ ├── context.go │ ├── context_test.go │ ├── ginkgo_test.go │ └── pkg.go └── rinqamqp ├── dialer.go ├── dialer_example_test.go ├── ginkgo_test.go ├── internal ├── amqputil │ ├── channel_pool.go │ ├── deadline.go │ ├── deadline_test.go │ ├── ginkgo_test.go │ ├── span.go │ ├── trace.go │ └── trace_test.go ├── commandamqp │ ├── debug_response.go │ ├── exchanges.go │ ├── factory.go │ ├── invoker.go │ ├── invoker_logging.go │ ├── message.go │ ├── priority.go │ ├── queues.go │ ├── response.go │ ├── server.go │ └── server_logging.go └── notifyamqp │ ├── exchanges.go │ ├── factory.go │ ├── listener.go │ ├── listener_logging.go │ ├── message.go │ ├── notifier.go │ ├── notifier_logging.go │ └── queues.go ├── peer.go ├── peer_functional_test.go ├── peer_logging.go └── pkg.go /.errignore: -------------------------------------------------------------------------------- 1 | (github.com/rinq/rinq-go/src/rinq.Response).Fail 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **Rinq** is open source software; contributions from the community are 4 | encouraged and appreciated. Please take a moment to read these guidelines 5 | before submitting changes. 6 | 7 | ## Requirements 8 | 9 | - [Go 1.9](https://golang.org/) 10 | - [GNU make](https://www.gnu.org/software/make/) (or equivalent) 11 | 12 | ## Running the tests 13 | 14 | make 15 | 16 | The default target of the make file installs all necessary dependencies and runs 17 | the tests. 18 | 19 | Code coverage reports can be built with: 20 | 21 | make coverage 22 | 23 | To rebuild coverage reports and open them in a browser, use: 24 | 25 | make coverage-open 26 | 27 | ## Submitting changes 28 | 29 | Change requests are reviewed and accepted via pull-requests on GitHub. If you're 30 | unfamiliar with this process, please read the relevant GitHub documentation 31 | regarding [forking a repository](https://help.github.com/articles/fork-a-repo) 32 | and [using pull-requests](https://help.github.com/articles/using-pull-requests). 33 | 34 | Before submitting your pull-request (typically against the `master` branch), 35 | please run: 36 | 37 | make prepare 38 | 39 | To apply any automated code-style updates, run linting checks, run the tests and 40 | build coverage reports. Please ensure that your changes are tested and that a 41 | high level of code coverage is maintained. 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /artifacts 2 | /vendor 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | services: 3 | - rabbitmq 4 | go: '1.9' 5 | script: make ci -j 8 6 | after_script: bash <(curl -s https://codecov.io/bash) 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Next Release 4 | 5 | - **[IMPROVED]** `Revision.Refresh()` always returns a usable revision (outside of a network error) 6 | 7 | ## 0.7.0 (2018-02-03) 8 | 9 | - **[BC]** `Session.CurrentRevision()` no longer returns an error 10 | - **[BC]** `Session.Destroy()` no longer waits for pending calls, use `Session.Done()` to wait 11 | - **[BC]** Remove `Revision.Ref()` method 12 | - **[FIX]** Constraint serialisation no longer returns an in-use buffer to the pool 13 | - **[NEW]** Add `Revision.SessionID()` method 14 | 15 | ## 0.6.0 (2017-10-27) 16 | 17 | - **[BC]** Rename the `amqp` package to `rinqamqp` 18 | - **[BC]** Removed `rinq.ValidateNamespace()` 19 | - **[BC]** Panic, rather than return an error, when a "programmer error" is made, such as using an invalid namespace 20 | - **[BC]** Change `rinq.AttrTable` from a map to an interface 21 | - **[BC]** Expand notification constraints to a fully-fledged expression system 22 | - **[NEW]** Add `Revision.Clear()` which sets all attributes in a single namespace to the empty string 23 | - **[NEW]** Add message IDs to `rinq.Request` and `rinq.Notification` 24 | - **[NEW]** Add `trace.WithRoot()` which adds a custom trace ID to a context only if there is no existing trace ID 25 | - **[NEW]** Add `ident.MustValidate()` 26 | - **[FIX]** Payload now enocdes `nil` maps and slices as empty objects and arrays, respectively, instead of `null` 27 | 28 | ## 0.5.0 (2017-10-02) 29 | 30 | - **[BC]** Remove `Config` in favour of "functional options" in the `options` package 31 | - **[BC]** Session attributes are now namespaced 32 | - **[NEW]** Add support for [OpenTracing](https://opentracing.io) via new `options.Tracer()` option 33 | 34 | ## 0.4.0 (2017-09-18) 35 | 36 | Please note that this release includes changes to the definition of AMQP 37 | exchanges. The `ntf.uc` and `ntf.mc` exchanges will need to be deleted on the 38 | broker before starting a peer. 39 | 40 | - **[BC]** Remove `Session.ExecuteMany()` 41 | - **[BC]** Add namespaces to session notifications 42 | - **[BC]** Rename `Config.CommandPreFetch` to `CommandWorkers` 43 | - **[BC]** Rename `Config.SessionPreFetch` to `SessionWorkers` 44 | - **[FIX]** Fix race-condition caused by payload buffer "double-free" 45 | - **[NEW]** Add `amqp.DialEnv()` which connects to an AMQP Rinq network described by environment variables 46 | - **[NEW]** Add `NewConfigFromEnv()` which returns a Rinq configuration described by environment variables 47 | - **[NEW]** Add `Config.Product` which is passed to the broker in the AMQP handshake 48 | - **[FIX]** Honour the context deadline when dialing an AMQP broker 49 | - **[IMPROVED]** `Session.NotifyMany()` and `NotifyMany()` now return `context.Canceled` when the peer is stopping 50 | 51 | ## 0.3.0 (2017-04-07) 52 | 53 | - **[BC]** `AsyncResponseHandler` is now passed the session 54 | - **[NEW]** `Session.Execute[Many]` now supports context deadlines 55 | - **[NEW]** Promote `trace` module to public API 56 | - **[FIX]** Allow empty messages in failure responses 57 | - **[FIX]** Fix panic when when stopping a peer repeatedly 58 | 59 | ## 0.2.0 (2017-03-04) 60 | 61 | - **[BC]** Rename from "Overpass" to "Rinq" 62 | - **[BC]** Require Go 1.8 63 | - **[BC]** Rename `Session.Close()` and `Revision.Close()` to `Destroy()` 64 | - **[BC]** Move AMQP implementation into `amqp` sub-package 65 | - **[BC]** Move identifier types into `ident` sub-package 66 | - **[BC]** Renamed `Command` to `Request` 67 | - **[BC]** Renamed `Responder` to `Response` 68 | - **[BC]** `Peer.Stop()` and `GracefulStop()` no longer block 69 | - **[NEW]** Add `Session.CallAsync()` 70 | - **[IMPROVED]** AMQP broker capabilities are checked on connect 71 | - **[IMPROVED]** `Response.Fail()` accepts sprintf-style format specifier 72 | - **[IMPROVED]** Log all payload values when debug logging is enabled 73 | 74 | ## 0.1.0 (2017-02-24) 75 | 76 | - Initial release 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | **© 2014, [James Harris](https://github.com/jmalloc)** 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | **THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.** 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | -include artifacts/make/go.mk 3 | 4 | artifacts/make/%.mk: 5 | bash <(curl -s https://rinq.github.io/make/install) $* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rinq for Go 2 | 3 | [![Build Status](http://img.shields.io/travis/rinq/rinq-go/master.svg)](https://travis-ci.org/rinq/rinq-go) 4 | [![Code Coverage](https://img.shields.io/codecov/c/github/rinq/rinq-go/master.svg)](https://codecov.io/github/rinq/rinq-go) 5 | [![Latest Version](https://img.shields.io/github/tag/rinq/rinq-go.svg?label=semver)](https://semver.org) 6 | [![GoDoc](https://godoc.org/github.com/rinq/rinq-go?status.svg)](https://godoc.org/github.com/rinq/rinq-go/src/rinq) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/rinq/rinq-go)](https://goreportcard.com/report/github.com/rinq/rinq-go) 8 | 9 | [Rinq](http://rinq.io) is a cross-language command bus and distributed ephemeral data store. This 10 | repository provides an implementation of Rinq in Go. 11 | 12 | ## Building and testing 13 | 14 | Please see [CONTRIBUTING.md](.github/CONTRIBUTING.md) for information about 15 | running the tests and submitting changes. 16 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | target: auto 7 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 1318cd3a7336c47f7d1e583c51c3bd4a7b364bfa54a1186327ea24589be4236a 2 | updated: 2018-02-13T12:42:46.063902649+10:00 3 | imports: 4 | - name: github.com/hashicorp/go-version 5 | version: 03c5bf6be031b6dd45afec16b1cf94fc8938bc77 6 | - name: github.com/jmalloc/twelf 7 | version: ffe2c64ef8c5e93b1410e818808fe4fddfb5e02d 8 | - name: github.com/onsi/ginkgo 9 | version: 747514b53ddd06d5d37d096c1cb313cfe620d7d4 10 | subpackages: 11 | - config 12 | - extensions/table 13 | - internal/codelocation 14 | - internal/containernode 15 | - internal/failer 16 | - internal/leafnodes 17 | - internal/remote 18 | - internal/spec 19 | - internal/spec_iterator 20 | - internal/specrunner 21 | - internal/suite 22 | - internal/testingtproxy 23 | - internal/writer 24 | - reporters 25 | - reporters/stenographer 26 | - reporters/stenographer/support/go-colorable 27 | - reporters/stenographer/support/go-isatty 28 | - types 29 | - name: github.com/onsi/gomega 30 | version: a9c79f175573664afefe4fe0f3827c66263abcbb 31 | subpackages: 32 | - format 33 | - internal/assertion 34 | - internal/asyncassertion 35 | - internal/oraclematcher 36 | - internal/testingtsupport 37 | - matchers 38 | - matchers/support/goraph/bipartitegraph 39 | - matchers/support/goraph/edge 40 | - matchers/support/goraph/node 41 | - matchers/support/goraph/util 42 | - types 43 | - name: github.com/opentracing/opentracing-go 44 | version: 1949ddbfd147afd4d964a9f00b24eb291e0e7c38 45 | subpackages: 46 | - ext 47 | - log 48 | - name: github.com/streadway/amqp 49 | version: fc7fda2371f5327ad39211e09482845b8734cc72 50 | - name: github.com/ugorji/go 51 | version: 5efa3251c7f7d05e5d9704a69a984ec9f1386a40 52 | subpackages: 53 | - codec 54 | - name: golang.org/x/net 55 | version: 4f2fc6c1e69d41baf187332ee08fbd2b296f21ed 56 | subpackages: 57 | - context 58 | - html 59 | - html/atom 60 | - html/charset 61 | testImports: 62 | - name: github.com/davecgh/go-spew 63 | version: 346938d642f2ec3594ed81d874461961cd0faa76 64 | subpackages: 65 | - spew 66 | - name: github.com/uber/jaeger-client-go 67 | version: 3e3870040def0ebdaf65a003863fa64f5cb26139 68 | - name: golang.org/x/sys 69 | version: d9157a9621b69ad1d8d77a1933590c416593f24f 70 | subpackages: 71 | - unix 72 | - name: golang.org/x/text 73 | version: d69c40b4be55797923cec7457fac7a244d91a9b6 74 | subpackages: 75 | - encoding 76 | - encoding/charmap 77 | - encoding/internal 78 | - encoding/internal/identifier 79 | - encoding/japanese 80 | - encoding/korean 81 | - encoding/simplifiedchinese 82 | - encoding/traditionalchinese 83 | - encoding/unicode 84 | - internal/utf8internal 85 | - runes 86 | - transform 87 | - name: gopkg.in/yaml.v2 88 | version: bef53efd0c76e49e6de55ead051f886bea7e9420 89 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/rinq/rinq-go 2 | import: 3 | - package: github.com/streadway/amqp 4 | version: master 5 | - package: github.com/onsi/ginkgo 6 | version: master 7 | - package: github.com/onsi/gomega 8 | version: master 9 | - package: github.com/hashicorp/go-version 10 | - package: github.com/opentracing/opentracing-go 11 | version: ~1.0.2 12 | - package: github.com/jmalloc/twelf 13 | testImport: 14 | - package: github.com/uber/jaeger-client-go 15 | - package: github.com/davecgh/go-spew 16 | version: ~1.1.0 17 | subpackages: 18 | - spew 19 | -------------------------------------------------------------------------------- /src/internal/attributes/catalog.go: -------------------------------------------------------------------------------- 1 | package attributes 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/internal/x/bufferpool" 5 | "github.com/rinq/rinq-go/src/rinq/constraint" 6 | ) 7 | 8 | // Catalog is a namespaced collection of attributes. 9 | type Catalog map[string]VTable 10 | 11 | // WithNamespace returns a copy of the catalog with the ns namespace replaced by t. 12 | func (c Catalog) WithNamespace(ns string, t VTable) Catalog { 13 | r := Catalog{ns: t} 14 | 15 | for n, t := range c { 16 | if n != ns { 17 | r[n] = t.Clone() 18 | } 19 | } 20 | 21 | return r 22 | } 23 | 24 | // MatchConstraint returns true if con evalutes to true for the attributes in 25 | // attrs. The ns namespace is the default namespace used if there is no 'within' 26 | // constraint. 27 | func (c Catalog) MatchConstraint(ns string, con constraint.Constraint) bool { 28 | isMatch, _ := con.Accept(&catalogMatcher{c}, ns) 29 | return isMatch.(bool) 30 | } 31 | 32 | // IsEmpty returns true if there are no attributes in the catalog. 33 | func (c Catalog) IsEmpty() bool { 34 | for _, t := range c { 35 | if !t.IsEmpty() { 36 | return false 37 | } 38 | } 39 | 40 | return true 41 | } 42 | 43 | func (c Catalog) String() string { 44 | buf := bufferpool.Get() 45 | defer bufferpool.Put(buf) 46 | 47 | empty := true 48 | for ns, t := range c { 49 | if t.IsEmpty() { 50 | continue 51 | } 52 | 53 | if empty { 54 | empty = false 55 | } else { 56 | buf.WriteRune(' ') 57 | } 58 | 59 | buf.WriteString(ns) 60 | buf.WriteString("::") 61 | buf.WriteString(t.String()) 62 | } 63 | 64 | if empty { 65 | return "{}" 66 | } 67 | 68 | return buf.String() 69 | } 70 | -------------------------------------------------------------------------------- /src/internal/attributes/catalog_matcher.go: -------------------------------------------------------------------------------- 1 | package attributes 2 | 3 | import "github.com/rinq/rinq-go/src/rinq/constraint" 4 | 5 | // catalogMatcher is a constraint.Visitor that evaluates a constraint against an 6 | // attribute catalog. 7 | type catalogMatcher struct { 8 | cat Catalog 9 | } 10 | 11 | // unpackNamespace extracts the first element of args as a string. 12 | func unpackNamespace(args []interface{}) string { 13 | return args[0].(string) 14 | } 15 | 16 | func (m *catalogMatcher) None(_ ...interface{}) (interface{}, error) { 17 | return true, nil 18 | } 19 | 20 | func (m *catalogMatcher) Within(ns string, cons []constraint.Constraint, _ ...interface{}) (interface{}, error) { 21 | for _, con := range cons { 22 | isMatch, _ := con.Accept(m, ns) 23 | if !isMatch.(bool) { 24 | return false, nil 25 | } 26 | } 27 | 28 | return true, nil 29 | } 30 | 31 | func (m *catalogMatcher) Equal(k, v string, args ...interface{}) (interface{}, error) { 32 | ns := unpackNamespace(args) 33 | return m.cat[ns][k].Value == v, nil 34 | } 35 | 36 | func (m *catalogMatcher) NotEqual(k, v string, args ...interface{}) (interface{}, error) { 37 | ns := unpackNamespace(args) 38 | return m.cat[ns][k].Value != v, nil 39 | } 40 | 41 | func (m *catalogMatcher) Not(con constraint.Constraint, args ...interface{}) (interface{}, error) { 42 | isMatch, _ := con.Accept(m, args...) 43 | return !isMatch.(bool), nil 44 | } 45 | 46 | func (m *catalogMatcher) And(cons []constraint.Constraint, args ...interface{}) (interface{}, error) { 47 | for _, con := range cons { 48 | isMatch, _ := con.Accept(m, args...) 49 | if !isMatch.(bool) { 50 | return false, nil 51 | } 52 | } 53 | 54 | return true, nil 55 | } 56 | 57 | func (m *catalogMatcher) Or(cons []constraint.Constraint, args ...interface{}) (interface{}, error) { 58 | for _, con := range cons { 59 | isMatch, _ := con.Accept(m, args...) 60 | if isMatch.(bool) { 61 | return true, nil 62 | } 63 | } 64 | 65 | return false, nil 66 | } 67 | -------------------------------------------------------------------------------- /src/internal/attributes/collection.go: -------------------------------------------------------------------------------- 1 | package attributes 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/internal/x/bufferpool" 5 | "github.com/rinq/rinq-go/src/rinq" 6 | ) 7 | 8 | // Collection is a collection of attributes. 9 | type Collection interface { 10 | // Each calls fn for each attribute in the collection. Iteration stops 11 | // when fn returns false. 12 | Each(fn func(rinq.Attr) bool) 13 | 14 | // IsEmpty returns true if there are no attributes in the collection. 15 | IsEmpty() bool 16 | 17 | String() string 18 | } 19 | 20 | // ToMap returns a new map of attributes from attrs. 21 | func ToMap(attrs Collection) map[string]rinq.Attr { 22 | m := map[string]rinq.Attr{} 23 | 24 | attrs.Each(func(a rinq.Attr) bool { 25 | m[a.Key] = a 26 | return true 27 | }) 28 | 29 | return m 30 | } 31 | 32 | // ToString provides an implementation of Collection.String() using 33 | // Collection.Each(). 34 | func ToString(attrs Collection) string { 35 | buf := bufferpool.Get() 36 | defer bufferpool.Put(buf) 37 | 38 | buf.WriteRune('{') 39 | 40 | empty := true 41 | attrs.Each(func(attr rinq.Attr) bool { 42 | if empty { 43 | empty = false 44 | } else { 45 | buf.WriteString(", ") 46 | } 47 | 48 | buf.WriteString(attr.String()) 49 | return true 50 | }) 51 | 52 | buf.WriteRune('}') 53 | 54 | return buf.String() 55 | } 56 | -------------------------------------------------------------------------------- /src/internal/attributes/collection_test.go: -------------------------------------------------------------------------------- 1 | package attributes_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | . "github.com/rinq/rinq-go/src/internal/attributes" 7 | "github.com/rinq/rinq-go/src/rinq" 8 | ) 9 | 10 | var _ = Describe("ToMap", func() { 11 | It("returns a map containing the attributes", func() { 12 | table := Table{ 13 | "a": rinq.Set("a", "1"), 14 | "b": rinq.Set("b", "2"), 15 | } 16 | 17 | Expect(ToMap(table)).To(Equal( 18 | map[string]rinq.Attr{ 19 | "a": rinq.Set("a", "1"), 20 | "b": rinq.Set("b", "2"), 21 | }, 22 | )) 23 | }) 24 | }) 25 | 26 | // Describe("WriteTo", func() { 27 | // It("writes only braces when the list is empty", func() { 28 | // var buf bytes.Buffer 29 | // 30 | // List{}.WriteTo(&buf) 31 | // 32 | // Expect(buf.String()).To(Equal("{}")) 33 | // }) 34 | // 35 | // It("writes key/value pairs in order", func() { 36 | // var buf bytes.Buffer 37 | // l := List{ 38 | // rinq.Set("a", "1"), 39 | // rinq.Set("b", "2"), 40 | // } 41 | // 42 | // l.WriteTo(&buf) 43 | // 44 | // Expect(buf.String()).To(Equal("{a=1, b=2}")) 45 | // }) 46 | // }) 47 | // 48 | // Describe("String", func() { 49 | // It("returns only braces when the list is empty", func() { 50 | // Expect(List{}.String()).To(Equal("{}")) 51 | // }) 52 | // 53 | // It("returns key/value pairs in order", func() { 54 | // l := List{ 55 | // rinq.Set("a", "1"), 56 | // rinq.Set("b", "2"), 57 | // } 58 | // 59 | // Expect(l.String()).To(Equal("{a=1, b=2}")) 60 | // }) 61 | // }) 62 | -------------------------------------------------------------------------------- /src/internal/attributes/diff.go: -------------------------------------------------------------------------------- 1 | package attributes 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/internal/x/bufferpool" 5 | "github.com/rinq/rinq-go/src/rinq/ident" 6 | ) 7 | 8 | // Diff is an attribute collection representing a change to a set of attributes 9 | // within a single namespace. 10 | type Diff struct { 11 | VList 12 | 13 | Namespace string 14 | Revision ident.Revision 15 | } 16 | 17 | // NewDiff returns a new Diff for the given namespace. 18 | func NewDiff(ns string, rev ident.Revision) *Diff { 19 | return &Diff{ 20 | Namespace: ns, 21 | Revision: rev, 22 | } 23 | } 24 | 25 | // Append adds attributes to the diff. 26 | func (d *Diff) Append(a ...VAttr) { 27 | d.VList = append(d.VList, a...) 28 | } 29 | 30 | func (d *Diff) String() string { 31 | buf := bufferpool.Get() 32 | defer bufferpool.Put(buf) 33 | 34 | buf.WriteString(d.Namespace) 35 | buf.WriteString("::") 36 | buf.WriteString(d.StringWithoutNamespace()) 37 | 38 | return buf.String() 39 | } 40 | 41 | // StringWithoutNamespace returns a string representation of d, without the 42 | // namespace name. 43 | func (d *Diff) StringWithoutNamespace() string { 44 | buf := bufferpool.Get() 45 | defer bufferpool.Put(buf) 46 | 47 | buf.WriteRune('{') 48 | 49 | for index, attr := range d.VList { 50 | if index != 0 { 51 | buf.WriteString(", ") 52 | } 53 | 54 | if attr.CreatedAt == d.Revision { 55 | buf.WriteRune('+') 56 | } 57 | 58 | buf.WriteString(attr.String()) 59 | } 60 | 61 | buf.WriteRune('}') 62 | 63 | return buf.String() 64 | } 65 | -------------------------------------------------------------------------------- /src/internal/attributes/diff_test.go: -------------------------------------------------------------------------------- 1 | package attributes_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | . "github.com/rinq/rinq-go/src/internal/attributes" 7 | "github.com/rinq/rinq-go/src/rinq" 8 | ) 9 | 10 | var _ = Describe("Diff", func() { 11 | Describe("NewDiff", func() { 12 | It("sets the namespace", func() { 13 | d := NewDiff("ns", 0) 14 | 15 | Expect(d.Namespace).To(Equal("ns")) 16 | }) 17 | }) 18 | 19 | Describe("Append", func() { 20 | It("appends the attributes", func() { 21 | d := NewDiff("ns", 0) 22 | attrs := VList{ 23 | {Attr: rinq.Set("a", "1")}, 24 | {Attr: rinq.Set("b", "2")}, 25 | } 26 | 27 | d.Append(attrs...) 28 | 29 | Expect(d.VList).To(Equal(attrs)) 30 | }) 31 | }) 32 | 33 | Describe("String", func() { 34 | Context("when the diff is empty", func() { 35 | It("returns the namespace and braces", func() { 36 | d := NewDiff("ns", 0) 37 | Expect(d.String()).To(Equal("ns::{}")) 38 | }) 39 | }) 40 | 41 | Context("when the diff contains attributes", func() { 42 | var d *Diff 43 | 44 | BeforeEach(func() { 45 | d = NewDiff("ns", 1) 46 | }) 47 | 48 | It("renders key/value pairs in order", func() { 49 | d.Append( 50 | VAttr{Attr: rinq.Set("a", "1")}, 51 | VAttr{Attr: rinq.Set("b", "2")}, 52 | ) 53 | 54 | Expect(d.String()).To(Equal("ns::{a=1, b=2}")) 55 | }) 56 | 57 | It("renders new attributes with a plus-sign", func() { 58 | d.Append(VAttr{ 59 | Attr: rinq.Set("a", "1"), 60 | CreatedAt: 1, 61 | }) 62 | 63 | Expect(d.String()).To(Equal("ns::{+a=1}")) 64 | }) 65 | 66 | It("renders existing attributes normally", func() { 67 | d.Revision = 2 68 | d.Append(VAttr{ 69 | Attr: rinq.Set("a", "1"), 70 | CreatedAt: 1, 71 | }) 72 | 73 | Expect(d.String()).To(Equal("ns::{a=1}")) 74 | }) 75 | }) 76 | }) 77 | 78 | Describe("StringWithoutNamespace", func() { 79 | Context("when the diff is empty", func() { 80 | It("returns the namespace and braces", func() { 81 | d := NewDiff("ns", 0) 82 | Expect(d.StringWithoutNamespace()).To(Equal("{}")) 83 | }) 84 | }) 85 | 86 | Context("when the diff contains attributes", func() { 87 | var d *Diff 88 | 89 | BeforeEach(func() { 90 | d = NewDiff("ns", 1) 91 | }) 92 | 93 | It("renders key/value pairs in order", func() { 94 | d.Append( 95 | VAttr{Attr: rinq.Set("a", "1")}, 96 | VAttr{Attr: rinq.Set("b", "2")}, 97 | ) 98 | 99 | Expect(d.StringWithoutNamespace()).To(Equal("{a=1, b=2}")) 100 | }) 101 | 102 | It("renders new attributes with a plus-sign", func() { 103 | d.Append(VAttr{ 104 | Attr: rinq.Set("a", "1"), 105 | CreatedAt: 1, 106 | }) 107 | 108 | Expect(d.StringWithoutNamespace()).To(Equal("{+a=1}")) 109 | }) 110 | 111 | It("renders existing attributes normally", func() { 112 | d.Revision = 2 113 | d.Append(VAttr{ 114 | Attr: rinq.Set("a", "1"), 115 | CreatedAt: 1, 116 | }) 117 | 118 | Expect(d.StringWithoutNamespace()).To(Equal("{a=1}")) 119 | }) 120 | }) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /src/internal/attributes/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package attributes_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "attributes") 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/attributes/list.go: -------------------------------------------------------------------------------- 1 | package attributes 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/rinq" 5 | ) 6 | 7 | // List is a sequence of attributes. 8 | type List []rinq.Attr 9 | 10 | // Each calls fn for each attribute in the collection. Iteration stops 11 | // when fn returns false. 12 | func (l List) Each(fn func(rinq.Attr) bool) { 13 | for _, attr := range l { 14 | if !fn(attr) { 15 | return 16 | } 17 | } 18 | } 19 | 20 | // IsEmpty returns true if there are no attributes in the collection. 21 | func (l List) IsEmpty() bool { 22 | return len(l) == 0 23 | } 24 | 25 | func (l List) String() string { 26 | return ToString(l) 27 | } 28 | -------------------------------------------------------------------------------- /src/internal/attributes/list_test.go: -------------------------------------------------------------------------------- 1 | package attributes_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | . "github.com/rinq/rinq-go/src/internal/attributes" 7 | "github.com/rinq/rinq-go/src/rinq" 8 | ) 9 | 10 | var _ = Describe("List", func() { 11 | var list List 12 | 13 | BeforeEach(func() { 14 | list = List{ 15 | rinq.Set("a", "1"), 16 | rinq.Set("b", "2"), 17 | } 18 | }) 19 | 20 | Describe("Each", func() { 21 | It("calls the function for each attribute in the table", func() { 22 | var attrs []rinq.Attr 23 | list.Each(func(attr rinq.Attr) bool { 24 | attrs = append(attrs, attr) 25 | return true 26 | }) 27 | 28 | Expect(attrs).To(ConsistOf( 29 | rinq.Set("a", "1"), 30 | rinq.Set("b", "2"), 31 | )) 32 | }) 33 | 34 | It("stops iteration if the function returns false", func() { 35 | var attrs []rinq.Attr 36 | list.Each(func(attr rinq.Attr) bool { 37 | attrs = append(attrs, attr) 38 | return false 39 | }) 40 | 41 | Expect(len(attrs)).To(Equal(1)) 42 | }) 43 | }) 44 | 45 | Describe("IsEmpty", func() { 46 | It("returns true when the table is empty", func() { 47 | Expect(Table{}.IsEmpty()).To(BeTrue()) 48 | }) 49 | 50 | It("returns false when the table is not empty", func() { 51 | Expect(list.IsEmpty()).To(BeFalse()) 52 | }) 53 | }) 54 | 55 | Describe("String", func() { 56 | It("returns a comma-separated string representation", func() { 57 | Expect(list.String()).To(Equal("{a=1, b=2}")) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/internal/attributes/table.go: -------------------------------------------------------------------------------- 1 | package attributes 2 | 3 | import "github.com/rinq/rinq-go/src/rinq" 4 | 5 | // Table is a simple map-based implementation of rinq.AttrTable 6 | type Table map[string]rinq.Attr 7 | 8 | // Get returns the attribute with key k. 9 | func (t Table) Get(k string) (rinq.Attr, bool) { 10 | attr, ok := t[k] 11 | return attr, ok 12 | } 13 | 14 | // Each calls fn for each attribute in the collection. Iteration stops 15 | // when fn returns false. 16 | func (t Table) Each(fn func(rinq.Attr) bool) { 17 | for _, attr := range t { 18 | if !fn(attr) { 19 | return 20 | } 21 | } 22 | } 23 | 24 | // IsEmpty returns true if there are no attributes in the table. 25 | func (t Table) IsEmpty() bool { 26 | return len(t) == 0 27 | } 28 | 29 | func (t Table) String() string { 30 | return ToString(t) 31 | } 32 | -------------------------------------------------------------------------------- /src/internal/attributes/table_test.go: -------------------------------------------------------------------------------- 1 | package attributes_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | . "github.com/rinq/rinq-go/src/internal/attributes" 7 | "github.com/rinq/rinq-go/src/rinq" 8 | ) 9 | 10 | var _ = Describe("Table", func() { 11 | var table Table 12 | 13 | BeforeEach(func() { 14 | table = Table{ 15 | "a": rinq.Set("a", "1"), 16 | "b": rinq.Set("b", "2"), 17 | } 18 | }) 19 | 20 | Describe("Get", func() { 21 | It("returns the attribute", func() { 22 | attr, _ := table.Get("a") 23 | 24 | Expect(attr).To(Equal(rinq.Set("a", "1"))) 25 | }) 26 | }) 27 | 28 | Describe("Each", func() { 29 | It("calls the function for each attribute in the table", func() { 30 | var attrs []rinq.Attr 31 | table.Each(func(attr rinq.Attr) bool { 32 | attrs = append(attrs, attr) 33 | return true 34 | }) 35 | 36 | Expect(attrs).To(ConsistOf( 37 | rinq.Set("a", "1"), 38 | rinq.Set("b", "2"), 39 | )) 40 | }) 41 | 42 | It("stops iteration if the function returns false", func() { 43 | var attrs []rinq.Attr 44 | table.Each(func(attr rinq.Attr) bool { 45 | attrs = append(attrs, attr) 46 | return false 47 | }) 48 | 49 | Expect(len(attrs)).To(Equal(1)) 50 | }) 51 | }) 52 | 53 | Describe("IsEmpty", func() { 54 | It("returns true when the table is empty", func() { 55 | Expect(Table{}.IsEmpty()).To(BeTrue()) 56 | }) 57 | 58 | It("returns false when the table is not empty", func() { 59 | Expect(table.IsEmpty()).To(BeFalse()) 60 | }) 61 | }) 62 | 63 | Describe("String", func() { 64 | It("returns a comma-separated string representation", func() { 65 | Expect(table.String()).To(SatisfyAny( 66 | Equal("{a=1, b=2}"), 67 | Equal("{b=2, a=1}"), 68 | )) 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /src/internal/attributes/vattr.go: -------------------------------------------------------------------------------- 1 | package attributes 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/rinq" 5 | "github.com/rinq/rinq-go/src/rinq/ident" 6 | ) 7 | 8 | // VAttr is rinq.Attr with additional revision information. 9 | type VAttr struct { 10 | rinq.Attr 11 | 12 | CreatedAt ident.Revision `json:"cr,omitempty"` 13 | UpdatedAt ident.Revision `json:"ur,omitempty"` 14 | } 15 | -------------------------------------------------------------------------------- /src/internal/attributes/vlist.go: -------------------------------------------------------------------------------- 1 | package attributes 2 | 3 | import "github.com/rinq/rinq-go/src/rinq" 4 | 5 | // VList is a sequence of attributes with revision information. 6 | type VList []VAttr 7 | 8 | // Each calls fn for each attribute in the collection. Iteration stops 9 | // when fn returns false. 10 | func (l VList) Each(fn func(rinq.Attr) bool) { 11 | for _, attr := range l { 12 | if !fn(attr.Attr) { 13 | return 14 | } 15 | } 16 | } 17 | 18 | // IsEmpty returns true if there are no attributes in the collection. 19 | func (l VList) IsEmpty() bool { 20 | return len(l) == 0 21 | } 22 | 23 | func (l VList) String() string { 24 | return ToString(l) 25 | } 26 | -------------------------------------------------------------------------------- /src/internal/attributes/vlist_test.go: -------------------------------------------------------------------------------- 1 | package attributes_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | . "github.com/rinq/rinq-go/src/internal/attributes" 7 | "github.com/rinq/rinq-go/src/rinq" 8 | ) 9 | 10 | var _ = Describe("VList", func() { 11 | var list VList 12 | 13 | BeforeEach(func() { 14 | list = VList{ 15 | {Attr: rinq.Set("a", "1")}, 16 | {Attr: rinq.Set("b", "2")}, 17 | } 18 | }) 19 | 20 | Describe("Each", func() { 21 | It("calls the function for each attribute in the table", func() { 22 | var attrs []rinq.Attr 23 | list.Each(func(attr rinq.Attr) bool { 24 | attrs = append(attrs, attr) 25 | return true 26 | }) 27 | 28 | Expect(attrs).To(ConsistOf( 29 | rinq.Set("a", "1"), 30 | rinq.Set("b", "2"), 31 | )) 32 | }) 33 | 34 | It("stops iteration if the function returns false", func() { 35 | var attrs []rinq.Attr 36 | list.Each(func(attr rinq.Attr) bool { 37 | attrs = append(attrs, attr) 38 | return false 39 | }) 40 | 41 | Expect(len(attrs)).To(Equal(1)) 42 | }) 43 | }) 44 | 45 | Describe("IsEmpty", func() { 46 | It("returns true when the table is empty", func() { 47 | Expect(VList{}.IsEmpty()).To(BeTrue()) 48 | }) 49 | 50 | It("returns false when the table is not empty", func() { 51 | Expect(list.IsEmpty()).To(BeFalse()) 52 | }) 53 | }) 54 | 55 | Describe("String", func() { 56 | It("returns a comma-separated string representation", func() { 57 | Expect(list.String()).To(Equal("{a=1, b=2}")) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/internal/attributes/vtable.go: -------------------------------------------------------------------------------- 1 | package attributes 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/rinq" 5 | ) 6 | 7 | // VTable is a collection of attributes with revision information. 8 | type VTable map[string]VAttr 9 | 10 | // Each calls fn for each attribute in the collection. Iteration stops 11 | // when fn returns false. 12 | func (t VTable) Each(fn func(rinq.Attr) bool) { 13 | for _, attr := range t { 14 | if !fn(attr.Attr) { 15 | return 16 | } 17 | } 18 | } 19 | 20 | // IsEmpty returns true if there are no attributes in the table. 21 | func (t VTable) IsEmpty() bool { 22 | return len(t) == 0 23 | } 24 | 25 | func (t VTable) String() string { 26 | return ToString(t) 27 | } 28 | 29 | // Clone returns a copy of t. 30 | func (t VTable) Clone() VTable { 31 | c := VTable{} 32 | 33 | for k, v := range t { 34 | c[k] = v 35 | } 36 | 37 | return c 38 | } 39 | -------------------------------------------------------------------------------- /src/internal/attributes/vtable_test.go: -------------------------------------------------------------------------------- 1 | package attributes_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | . "github.com/rinq/rinq-go/src/internal/attributes" 7 | "github.com/rinq/rinq-go/src/rinq" 8 | ) 9 | 10 | var _ = Describe("VTable", func() { 11 | var table VTable 12 | 13 | BeforeEach(func() { 14 | table = VTable{ 15 | "a": {Attr: rinq.Set("a", "1")}, 16 | "b": {Attr: rinq.Set("b", "2")}, 17 | } 18 | }) 19 | 20 | Describe("Each", func() { 21 | It("calls the function for each attribute in the table", func() { 22 | var attrs []rinq.Attr 23 | table.Each(func(attr rinq.Attr) bool { 24 | attrs = append(attrs, attr) 25 | return true 26 | }) 27 | 28 | Expect(attrs).To(ConsistOf( 29 | rinq.Set("a", "1"), 30 | rinq.Set("b", "2"), 31 | )) 32 | }) 33 | 34 | It("stops iteration if the function returns false", func() { 35 | var attrs []rinq.Attr 36 | table.Each(func(attr rinq.Attr) bool { 37 | attrs = append(attrs, attr) 38 | return false 39 | }) 40 | 41 | Expect(len(attrs)).To(Equal(1)) 42 | }) 43 | }) 44 | 45 | Describe("IsEmpty", func() { 46 | It("returns true when the table is empty", func() { 47 | Expect(Table{}.IsEmpty()).To(BeTrue()) 48 | }) 49 | 50 | It("returns false when the table is not empty", func() { 51 | Expect(table.IsEmpty()).To(BeFalse()) 52 | }) 53 | }) 54 | 55 | Describe("String", func() { 56 | It("returns a comma-separated string representation", func() { 57 | Expect(table.String()).To(SatisfyAny( 58 | Equal("{a=1, b=2}"), 59 | Equal("{b=2, a=1}"), 60 | )) 61 | }) 62 | }) 63 | 64 | Describe("Clone", func() { 65 | It("returns a different instance", func() { 66 | t := table.Clone() 67 | t["c"] = VAttr{Attr: rinq.Set("c", "3")} 68 | 69 | Expect(table).NotTo(HaveKey("c")) 70 | }) 71 | 72 | It("contains the same attributes", func() { 73 | t := table.Clone() 74 | 75 | Expect(t).To(Equal(table)) 76 | }) 77 | 78 | It("returns a non-nil table when cloning a nil table", func() { 79 | table = nil 80 | t := table.Clone() 81 | 82 | Expect(t).To(BeEmpty()) 83 | Expect(t).NotTo(BeNil()) 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /src/internal/command/invoker.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rinq/rinq-go/src/internal/service" 7 | "github.com/rinq/rinq-go/src/rinq" 8 | "github.com/rinq/rinq-go/src/rinq/ident" 9 | ) 10 | 11 | // Invoker is a low-level RPC interface, it is used to implement the 12 | // "command subsystem", as well as internal peer-to-peer requests. 13 | // 14 | // The terminology "call" refers to an invocation that expects a response, 15 | // whereas "execute" is an invocation where no response is required. 16 | type Invoker interface { 17 | service.Service 18 | 19 | // CallUnicast sends a unicast command request to a specific peer and blocks 20 | // until a response is received or the context deadline is met. 21 | CallUnicast( 22 | ctx context.Context, 23 | msgID ident.MessageID, 24 | traceID string, 25 | target ident.PeerID, 26 | namespace string, 27 | command string, 28 | payload *rinq.Payload, 29 | ) (*rinq.Payload, error) 30 | 31 | // CallBalanced sends a load-balanced command request to the first available 32 | // peer and blocks until a response is received or the context deadline is met. 33 | CallBalanced( 34 | ctx context.Context, 35 | msgID ident.MessageID, 36 | traceID string, 37 | namespace string, 38 | command string, 39 | payload *rinq.Payload, 40 | ) (*rinq.Payload, error) 41 | 42 | // CallBalancedAsync sends a load-balanced command request to the first 43 | // available peer, instructs it to send a response, but does not block. 44 | CallBalancedAsync( 45 | ctx context.Context, 46 | msgID ident.MessageID, 47 | traceID string, 48 | namespace string, 49 | command string, 50 | payload *rinq.Payload, 51 | ) error 52 | 53 | // SetAsyncHandler sets the asynchronous handler to use for a specific 54 | // session. 55 | SetAsyncHandler(sessID ident.SessionID, h rinq.AsyncHandler) 56 | 57 | // ExecuteBalanced sends a load-balanced command request to the first 58 | // available peer and returns immediately. 59 | ExecuteBalanced( 60 | ctx context.Context, 61 | msgID ident.MessageID, 62 | traceID string, 63 | namespace string, 64 | command string, 65 | payload *rinq.Payload, 66 | ) error 67 | 68 | // ExecuteMulticast sends a multicast command request to the all available 69 | // peers and returns immediately. 70 | ExecuteMulticast( 71 | ctx context.Context, 72 | msgID ident.MessageID, 73 | traceID string, 74 | namespace string, 75 | command string, 76 | payload *rinq.Payload, 77 | ) error 78 | } 79 | -------------------------------------------------------------------------------- /src/internal/command/response.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jmalloc/twelf/src/twelf" 7 | opentracing "github.com/opentracing/opentracing-go" 8 | "github.com/rinq/rinq-go/src/internal/opentr" 9 | "github.com/rinq/rinq-go/src/rinq" 10 | "github.com/rinq/rinq-go/src/rinq/ident" 11 | ) 12 | 13 | // response wraps a "parent" response and performs logging and tracing when the 14 | // response is closed. 15 | type response struct { 16 | req rinq.Request 17 | res rinq.Response 18 | 19 | peerID ident.PeerID 20 | traceID string 21 | logger twelf.Logger 22 | span opentracing.Span 23 | startedAt time.Time 24 | } 25 | 26 | // NewResponse returns a response that wraps res. 27 | func NewResponse( 28 | req rinq.Request, 29 | res rinq.Response, 30 | peerID ident.PeerID, 31 | traceID string, 32 | logger twelf.Logger, 33 | span opentracing.Span, 34 | ) rinq.Response { 35 | return &response{ 36 | res: res, 37 | req: req, 38 | 39 | peerID: peerID, 40 | traceID: traceID, 41 | logger: logger, 42 | span: span, 43 | startedAt: time.Now(), 44 | } 45 | } 46 | 47 | func (r *response) IsRequired() bool { 48 | return r.res.IsRequired() 49 | } 50 | 51 | func (r *response) IsClosed() bool { 52 | return r.res.IsClosed() 53 | } 54 | 55 | func (r *response) Done(payload *rinq.Payload) { 56 | r.res.Done(payload) 57 | r.logSuccess(payload) 58 | 59 | opentr.LogServerSuccess(r.span, payload) 60 | } 61 | 62 | func (r *response) Error(err error) { 63 | r.res.Error(err) 64 | 65 | if failure, ok := err.(rinq.Failure); ok { 66 | r.logFailure(failure.Type, failure.Payload) 67 | } else { 68 | r.logError(err) 69 | } 70 | 71 | opentr.LogServerError(r.span, err) 72 | } 73 | 74 | func (r *response) Fail(f, t string, v ...interface{}) rinq.Failure { 75 | err := r.res.Fail(f, t, v...) 76 | r.logFailure(f, nil) 77 | opentr.LogServerError(r.span, err) 78 | 79 | return err 80 | } 81 | 82 | func (r *response) Close() bool { 83 | if r.res.Close() { 84 | r.logSuccess(nil) 85 | opentr.LogServerSuccess(r.span, nil) 86 | return true 87 | } 88 | 89 | return false 90 | } 91 | 92 | func (r *response) logSuccess(payload *rinq.Payload) { 93 | r.logger.Log( 94 | "%s handled '%s::%s' command from %s successfully (%dms %d/i %d/o) [%s]", 95 | r.peerID.ShortString(), 96 | r.req.Namespace, 97 | r.req.Command, 98 | r.req.ID.Ref.ShortString(), 99 | time.Since(r.startedAt)/time.Millisecond, 100 | r.req.Payload.Len(), 101 | payload.Len(), 102 | r.traceID, 103 | ) 104 | } 105 | 106 | func (r *response) logFailure(failureType string, payload *rinq.Payload) { 107 | r.logger.Log( 108 | "%s handled '%s::%s' command from %s: '%s' failure (%dms %d/i %d/o) [%s]", 109 | r.peerID.ShortString(), 110 | r.req.Namespace, 111 | r.req.Command, 112 | r.req.ID.Ref.ShortString(), 113 | failureType, 114 | time.Since(r.startedAt)/time.Millisecond, 115 | r.req.Payload.Len(), 116 | payload.Len(), 117 | r.traceID, 118 | ) 119 | } 120 | 121 | func (r *response) logError(err error) { 122 | r.logger.Log( 123 | "%s handled '%s::%s' command from %s: '%s' error (%dms %d/i 0/o) [%s]", 124 | r.peerID.ShortString(), 125 | r.req.Namespace, 126 | r.req.Command, 127 | r.req.ID.Ref.ShortString(), 128 | err, 129 | time.Since(r.startedAt)/time.Millisecond, 130 | r.req.Payload.Len(), 131 | r.traceID, 132 | ) 133 | } 134 | -------------------------------------------------------------------------------- /src/internal/command/server.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/internal/service" 5 | "github.com/rinq/rinq-go/src/rinq" 6 | ) 7 | 8 | // Server processes command requests made by an invoker. 9 | type Server interface { 10 | service.Service 11 | 12 | Listen(ns string, h rinq.CommandHandler) (bool, error) 13 | Unlisten(ns string) (bool, error) 14 | } 15 | -------------------------------------------------------------------------------- /src/internal/functest/canned_responses.go: -------------------------------------------------------------------------------- 1 | package functest 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/rinq/rinq-go/src/rinq" 8 | ) 9 | 10 | // AlwaysReturn creates a CommandHandler that already responds with v. 11 | func AlwaysReturn(v interface{}) rinq.CommandHandler { 12 | p := rinq.NewPayload(v) 13 | 14 | return func(ctx context.Context, req rinq.Request, res rinq.Response) { 15 | req.Payload.Close() 16 | res.Done(p) 17 | } 18 | } 19 | 20 | // AlwaysPanic creates a CommandHandler that already responds with v. 21 | func AlwaysPanic() rinq.CommandHandler { 22 | return func(ctx context.Context, req rinq.Request, res rinq.Response) { 23 | req.Payload.Close() 24 | res.Close() 25 | panic("functest.AlwaysPanic!") 26 | } 27 | } 28 | 29 | // CloseAfter creates a CommandHandler that closes the response after a timeout. 30 | func CloseAfter(d time.Duration) rinq.CommandHandler { 31 | return func(ctx context.Context, req rinq.Request, res rinq.Response) { 32 | req.Payload.Close() 33 | time.Sleep(d) 34 | res.Close() 35 | } 36 | } 37 | 38 | // Barrier create a command handler that attempts to write to ch twice. 39 | func Barrier(ch chan<- struct{}) rinq.CommandHandler { 40 | return BarrierN(ch, 2) 41 | } 42 | 43 | // BarrierN create a command handler that attempts to write to ch n times. 44 | func BarrierN(ch chan<- struct{}, n int) rinq.CommandHandler { 45 | return func(ctx context.Context, req rinq.Request, res rinq.Response) { 46 | req.Payload.Close() 47 | defer res.Close() 48 | 49 | for i := 0; i < n; i++ { 50 | select { 51 | case ch <- struct{}{}: 52 | case <-ctx.Done(): 53 | return 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/internal/functest/must.go: -------------------------------------------------------------------------------- 1 | package functest 2 | 3 | // Must panics if the right-most argument is a non-nil error. 4 | func Must(v ...interface{}) { 5 | if len(v) == 0 { 6 | return 7 | } 8 | 9 | err, _ := v[len(v)-1].(error) 10 | if err != nil { 11 | panic(err) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/functest/namespaces.go: -------------------------------------------------------------------------------- 1 | package functest 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | 8 | "github.com/streadway/amqp" 9 | ) 10 | 11 | var namespaces struct { 12 | mutex sync.Mutex 13 | count int 14 | names map[string]struct{} 15 | broker *amqp.Connection 16 | channel *amqp.Channel 17 | } 18 | 19 | // NewNamespace returns a string that is a valid namespace. 20 | func NewNamespace() string { 21 | namespaces.mutex.Lock() 22 | defer namespaces.mutex.Unlock() 23 | 24 | namespaces.count++ 25 | ns := fmt.Sprintf("rinq-test-%d-%d", os.Getpid(), namespaces.count) 26 | 27 | if namespaces.names == nil { 28 | namespaces.names = map[string]struct{}{} 29 | } 30 | 31 | namespaces.names[ns] = struct{}{} 32 | 33 | return ns 34 | } 35 | 36 | // TearDownNamespaces cleans up any queues created for command namespaces made 37 | // via NewNamespace() 38 | func TearDownNamespaces() { 39 | namespaces.mutex.Lock() 40 | defer namespaces.mutex.Unlock() 41 | 42 | if len(namespaces.names) == 0 { 43 | return 44 | } 45 | 46 | if namespaces.channel == nil { 47 | dsn := os.Getenv("RINQ_AMQP_DSN") 48 | if dsn == "" { 49 | dsn = "amqp://localhost" 50 | } 51 | 52 | broker, err := amqp.Dial(dsn) 53 | if err != nil { 54 | fmt.Println(err) 55 | return 56 | } 57 | 58 | channel, err := broker.Channel() 59 | if err != nil { 60 | _ = broker.Close() 61 | fmt.Println(err) 62 | return 63 | } 64 | 65 | namespaces.broker = broker 66 | namespaces.channel = channel 67 | } 68 | 69 | for ns := range namespaces.names { 70 | _, err := namespaces.channel.QueueDelete( 71 | "cmd."+ns, // see commandamqp.balancedRequestQueue() 72 | false, // ifUnused, 73 | false, // ifEmpty, 74 | false, // noWait 75 | ) 76 | if err != nil { 77 | namespaces.broker = nil 78 | namespaces.channel = nil 79 | fmt.Println(err) 80 | return 81 | } 82 | 83 | delete(namespaces.names, ns) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/internal/functest/peer.go: -------------------------------------------------------------------------------- 1 | package functest 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/jmalloc/twelf/src/twelf" 7 | "github.com/rinq/rinq-go/src/rinq" 8 | "github.com/rinq/rinq-go/src/rinq/options" 9 | "github.com/rinq/rinq-go/src/rinqamqp" 10 | ) 11 | 12 | var sharedPeer struct { 13 | mutex sync.Mutex 14 | peer rinq.Peer 15 | } 16 | 17 | // SharedPeer returns a peer for use in functional tests. 18 | func SharedPeer() rinq.Peer { 19 | sharedPeer.mutex.Lock() 20 | defer sharedPeer.mutex.Unlock() 21 | 22 | if sharedPeer.peer == nil { 23 | sharedPeer.peer = NewPeer() 24 | } 25 | 26 | return sharedPeer.peer 27 | } 28 | 29 | // NewPeer returns a new peer for use in functional tests. 30 | func NewPeer() rinq.Peer { 31 | peer, err := rinqamqp.DialEnv( 32 | options.Logger( 33 | &twelf.StandardLogger{CaptureDebug: true}, 34 | ), 35 | ) 36 | 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | return peer 42 | } 43 | -------------------------------------------------------------------------------- /src/internal/functest/pkg.go: -------------------------------------------------------------------------------- 1 | // Package functest contains utilities for writing functional tests for Rinq. 2 | package functest 3 | -------------------------------------------------------------------------------- /src/internal/localsession/revision.go: -------------------------------------------------------------------------------- 1 | package localsession 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmalloc/twelf/src/twelf" 7 | "github.com/rinq/rinq-go/src/internal/attributes" 8 | "github.com/rinq/rinq-go/src/internal/namespaces" 9 | "github.com/rinq/rinq-go/src/rinq" 10 | "github.com/rinq/rinq-go/src/rinq/ident" 11 | "github.com/rinq/rinq-go/src/rinq/trace" 12 | ) 13 | 14 | type revision struct { 15 | ref ident.Ref 16 | session *Session 17 | attrs attributes.Catalog 18 | logger twelf.Logger 19 | } 20 | 21 | func (r *revision) SessionID() ident.SessionID { 22 | return r.ref.ID 23 | } 24 | 25 | func (r *revision) Refresh(ctx context.Context) (rinq.Revision, error) { 26 | return r.session.CurrentRevision(), nil 27 | } 28 | 29 | func (r *revision) Get(ctx context.Context, ns, key string) (rinq.Attr, error) { 30 | namespaces.MustValidate(ns) 31 | 32 | if r.ref.Rev == 0 { 33 | return rinq.Attr{Key: key}, nil 34 | } 35 | 36 | attr, ok := r.attrs[ns][key] 37 | 38 | // The attribute hadn't yet been created at this revision. 39 | if !ok || attr.CreatedAt > r.ref.Rev { 40 | return rinq.Attr{Key: key}, nil 41 | } 42 | 43 | // The attribute exists, but has been updated since this revision. 44 | // The value at this revision is no longer known. 45 | if attr.UpdatedAt > r.ref.Rev { 46 | return rinq.Attr{}, rinq.StaleFetchError{Ref: r.ref} 47 | } 48 | 49 | return attr.Attr, nil 50 | } 51 | 52 | func (r *revision) GetMany(ctx context.Context, ns string, keys ...string) (rinq.AttrTable, error) { 53 | namespaces.MustValidate(ns) 54 | 55 | attrs := r.attrs[ns] 56 | table := attributes.Table{} 57 | 58 | for _, key := range keys { 59 | attr, ok := attrs[key] 60 | 61 | if !ok || attr.CreatedAt > r.ref.Rev { 62 | // The attribute hadn't yet been created at this revision. 63 | table[key] = rinq.Attr{Key: key} 64 | } else if attr.UpdatedAt <= r.ref.Rev { 65 | // The attribute was updated before this revision, it's still valid. 66 | table[key] = attr.Attr 67 | } else { 68 | return nil, rinq.StaleFetchError{Ref: r.ref} 69 | } 70 | } 71 | 72 | return table, nil 73 | } 74 | 75 | func (r *revision) Update(ctx context.Context, ns string, attrs ...rinq.Attr) (rinq.Revision, error) { 76 | namespaces.MustValidate(ns) 77 | 78 | if len(attrs) == 0 { 79 | return r, nil 80 | } 81 | 82 | rev, diff, err := r.session.TryUpdate(r.ref.Rev, ns, attrs) 83 | if err != nil { 84 | return r, err 85 | } 86 | 87 | logUpdate(ctx, r.logger, r.ref.ID.At(diff.Revision), diff) 88 | 89 | return rev, nil 90 | } 91 | 92 | func (r *revision) Clear(ctx context.Context, ns string) (rinq.Revision, error) { 93 | namespaces.MustValidate(ns) 94 | 95 | rev, diff, err := r.session.TryClear(r.ref.Rev, ns) 96 | if err != nil { 97 | return r, err 98 | } 99 | 100 | logClear(ctx, r.logger, r.ref.ID.At(diff.Revision), diff) 101 | 102 | return rev, nil 103 | } 104 | 105 | func (r *revision) Destroy(ctx context.Context) error { 106 | first, err := r.session.TryDestroy(r.ref.Rev) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | if first { 112 | logSessionDestroy(r.logger, r.ref, r.attrs, trace.Get(ctx)) 113 | } 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /src/internal/localsession/revision_logging.go: -------------------------------------------------------------------------------- 1 | package localsession 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmalloc/twelf/src/twelf" 7 | "github.com/rinq/rinq-go/src/internal/attributes" 8 | "github.com/rinq/rinq-go/src/rinq/ident" 9 | "github.com/rinq/rinq-go/src/rinq/trace" 10 | ) 11 | 12 | func logUpdate( 13 | ctx context.Context, 14 | logger twelf.Logger, 15 | ref ident.Ref, 16 | diff *attributes.Diff, 17 | ) { 18 | if traceID := trace.Get(ctx); traceID != "" { 19 | logger.Log( 20 | "%s session updated %s [%s]", 21 | ref.ShortString(), 22 | diff, 23 | traceID, 24 | ) 25 | } else { 26 | logger.Log( 27 | "%s session updated %s", 28 | ref.ShortString(), 29 | diff, 30 | ) 31 | } 32 | } 33 | 34 | func logClear( 35 | ctx context.Context, 36 | logger twelf.Logger, 37 | ref ident.Ref, 38 | diff *attributes.Diff, 39 | ) { 40 | if traceID := trace.Get(ctx); traceID != "" { 41 | logger.Log( 42 | "%s session cleared %s [%s]", 43 | ref.ShortString(), 44 | diff, 45 | traceID, 46 | ) 47 | } else { 48 | logger.Log( 49 | "%s session cleared %s", 50 | ref.ShortString(), 51 | diff, 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/internal/localsession/store.go: -------------------------------------------------------------------------------- 1 | package localsession 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/rinq/rinq-go/src/internal/revisions" 7 | "github.com/rinq/rinq-go/src/rinq" 8 | "github.com/rinq/rinq-go/src/rinq/ident" 9 | ) 10 | 11 | // Store is a collection of local sessions which provides an implementation 12 | // of revisions.Store. 13 | type Store struct { 14 | mutex sync.RWMutex 15 | sessions map[ident.SessionID]*Session 16 | } 17 | 18 | // NewStore returns a new session store. 19 | func NewStore() *Store { 20 | return &Store{ 21 | sessions: map[ident.SessionID]*Session{}, 22 | } 23 | } 24 | 25 | // Add adds a session to the store. 26 | func (s *Store) Add(sess *Session) { 27 | s.mutex.Lock() 28 | defer s.mutex.Unlock() 29 | 30 | s.sessions[sess.ID()] = sess 31 | } 32 | 33 | // Remove removes a session to from the store. 34 | func (s *Store) Remove(id ident.SessionID) { 35 | s.mutex.Lock() 36 | defer s.mutex.Unlock() 37 | 38 | delete(s.sessions, id) 39 | } 40 | 41 | // Get fetches a session from the store by its ID. 42 | func (s *Store) Get(id ident.SessionID) (sess *Session, ok bool) { 43 | s.mutex.RLock() 44 | defer s.mutex.RUnlock() 45 | 46 | sess, ok = s.sessions[id] 47 | return 48 | } 49 | 50 | // Each calls fn(sess) for each session in the store. 51 | func (s *Store) Each(fn func(*Session)) { 52 | s.mutex.RLock() 53 | defer s.mutex.RUnlock() 54 | 55 | for _, sess := range s.sessions { 56 | fn(sess) 57 | } 58 | } 59 | 60 | // GetRevision returns the session revision for the given ref. 61 | func (s *Store) GetRevision(ref ident.Ref) (rinq.Revision, error) { 62 | s.mutex.RLock() 63 | defer s.mutex.RUnlock() 64 | 65 | if sess, ok := s.sessions[ref.ID]; ok { 66 | return sess.At(ref.Rev) 67 | } 68 | 69 | return revisions.Closed(ref.ID), nil 70 | } 71 | -------------------------------------------------------------------------------- /src/internal/namespaces/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package namespaces_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "namespaces") 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/namespaces/validate.go: -------------------------------------------------------------------------------- 1 | package namespaces 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | ) 8 | 9 | // Validate checks if ns is a valid namespace. 10 | // 11 | // Namespaces must not be empty. Valid characters are alpha-numeric characters, 12 | // underscores, hyphens, periods and colons. 13 | // 14 | // Namespaces beginning with an underscore are reserved for internal use. 15 | // 16 | // The return value is nil if ns is a valid, unreserved namespace. 17 | func Validate(ns string) error { 18 | if ns == "" { 19 | return errors.New("namespace must not be empty") 20 | } else if ns[0] == '_' { 21 | return fmt.Errorf("namespace '%s' is reserved", ns) 22 | } else if !pattern.MatchString(ns) { 23 | return fmt.Errorf("namespace '%s' contains invalid characters", ns) 24 | } 25 | 26 | return nil 27 | } 28 | 29 | // MustValidate panics if ns is invalid. 30 | func MustValidate(ns string) { 31 | if err := Validate(ns); err != nil { 32 | panic(err) 33 | } 34 | } 35 | 36 | var pattern *regexp.Regexp 37 | 38 | func init() { 39 | pattern = regexp.MustCompile(`^[A-Za-z0-9_\.\-:]+$`) 40 | } 41 | -------------------------------------------------------------------------------- /src/internal/namespaces/validate_test.go: -------------------------------------------------------------------------------- 1 | package namespaces_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/extensions/table" 5 | . "github.com/onsi/gomega" 6 | "github.com/rinq/rinq-go/src/internal/namespaces" 7 | ) 8 | 9 | var entries = []TableEntry{ 10 | Entry("all valid characters", ":Aa3-_.", ""), 11 | Entry("typical style", "foo.bar.v1", ""), 12 | Entry("empty", "", "namespace must not be empty"), 13 | Entry("underscore", "_", "namespace '_' is reserved"), 14 | Entry("leading underscore", "_foo", "namespace '_foo' is reserved"), 15 | Entry("invalid characters", "foo bar", "namespace 'foo bar' contains invalid characters"), 16 | } 17 | 18 | var _ = DescribeTable( 19 | "Validate", 20 | func(namespace string, expected string) { 21 | err := namespaces.Validate(namespace) 22 | 23 | if expected == "" { 24 | Expect(err).ShouldNot(HaveOccurred()) 25 | } else { 26 | Expect(err.Error()).To(Equal(expected)) 27 | } 28 | }, 29 | entries..., 30 | ) 31 | 32 | var _ = DescribeTable( 33 | "MustValidate", 34 | func(namespace string, expected string) { 35 | fn := func() { 36 | namespaces.MustValidate(namespace) 37 | } 38 | 39 | if expected == "" { 40 | Expect(fn).ShouldNot(Panic()) 41 | } else { 42 | Expect(fn).Should(Panic()) 43 | } 44 | }, 45 | entries..., 46 | ) 47 | -------------------------------------------------------------------------------- /src/internal/notify/listener.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/internal/service" 5 | "github.com/rinq/rinq-go/src/rinq" 6 | "github.com/rinq/rinq-go/src/rinq/ident" 7 | ) 8 | 9 | // Listener accepts notifications sent by a notifier. 10 | type Listener interface { 11 | service.Service 12 | 13 | Listen(id ident.SessionID, ns string, h rinq.NotificationHandler) (bool, error) 14 | Unlisten(id ident.SessionID, ns string) (bool, error) 15 | UnlistenAll(id ident.SessionID) error 16 | } 17 | -------------------------------------------------------------------------------- /src/internal/notify/notifier.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rinq/rinq-go/src/rinq" 7 | "github.com/rinq/rinq-go/src/rinq/constraint" 8 | "github.com/rinq/rinq-go/src/rinq/ident" 9 | ) 10 | 11 | // Notifier is a low-level interface for sending notifications. 12 | type Notifier interface { 13 | // NotifyUnicast sends a notification to a specific session. 14 | NotifyUnicast( 15 | ctx context.Context, 16 | msgID ident.MessageID, 17 | traceID string, 18 | s ident.SessionID, 19 | ns string, 20 | t string, 21 | out *rinq.Payload, 22 | ) error 23 | 24 | // NotifyMulticast sends a notification to all sessions matching a constraint. 25 | NotifyMulticast( 26 | ctx context.Context, 27 | msgID ident.MessageID, 28 | traceID string, 29 | con constraint.Constraint, 30 | ns string, 31 | t string, 32 | out *rinq.Payload, 33 | ) error 34 | } 35 | -------------------------------------------------------------------------------- /src/internal/opentr/command.go: -------------------------------------------------------------------------------- 1 | package opentr 2 | 3 | import ( 4 | opentracing "github.com/opentracing/opentracing-go" 5 | "github.com/opentracing/opentracing-go/ext" 6 | "github.com/opentracing/opentracing-go/log" 7 | "github.com/rinq/rinq-go/src/internal/attributes" 8 | "github.com/rinq/rinq-go/src/rinq" 9 | "github.com/rinq/rinq-go/src/rinq/ident" 10 | ) 11 | 12 | var ( 13 | invokerCallEvent = log.String("event", "call") 14 | invokerCallAsyncEvent = log.String("event", "call-async") 15 | invokerExecuteEvent = log.String("event", "execute") 16 | 17 | invokerErrorSourceClient = log.String("error.source", "client") 18 | invokerErrorSourceServer = log.String("error.source", "server") 19 | 20 | invokerFailureEvent = log.String("event", "failure") 21 | 22 | serverRequestEvent = log.String("event", "request") 23 | serverResponseEvent = log.String("event", "response") 24 | ) 25 | 26 | // SetupCommand configures span as a command-related span. 27 | func SetupCommand( 28 | s opentracing.Span, 29 | id ident.MessageID, 30 | ns string, 31 | cmd string, 32 | ) { 33 | s.SetOperationName(ns + "::" + cmd + " command") 34 | 35 | s.SetTag("subsystem", "command") 36 | s.SetTag("message_id", id.String()) 37 | s.SetTag("namespace", ns) 38 | s.SetTag("command", cmd) 39 | } 40 | 41 | // LogInvokerCall logs information about a "call" style invocation to s. 42 | func LogInvokerCall( 43 | s opentracing.Span, 44 | attrs attributes.Catalog, 45 | p *rinq.Payload, 46 | ) { 47 | fields := []log.Field{ 48 | invokerCallEvent, 49 | log.Int("size", p.Len()), 50 | } 51 | 52 | if !attrs.IsEmpty() { 53 | fields = append(fields, lazyString("attributes", attrs.String)) 54 | } 55 | 56 | s.LogFields(fields...) 57 | } 58 | 59 | // LogInvokerCallAsync logs information about a "call-sync" style invocation to s. 60 | func LogInvokerCallAsync( 61 | s opentracing.Span, 62 | attrs attributes.Catalog, 63 | p *rinq.Payload, 64 | ) { 65 | fields := []log.Field{ 66 | invokerCallAsyncEvent, 67 | log.Int("size", p.Len()), 68 | } 69 | 70 | if !attrs.IsEmpty() { 71 | fields = append(fields, lazyString("attributes", attrs.String)) 72 | } 73 | 74 | s.LogFields(fields...) 75 | } 76 | 77 | // LogInvokerExecute logs information about an "execute" style invoation to s. 78 | func LogInvokerExecute( 79 | s opentracing.Span, 80 | attrs attributes.Catalog, 81 | p *rinq.Payload, 82 | ) { 83 | fields := []log.Field{ 84 | invokerExecuteEvent, 85 | log.Int("size", p.Len()), 86 | } 87 | 88 | if !attrs.IsEmpty() { 89 | fields = append(fields, lazyString("attributes", attrs.String)) 90 | } 91 | 92 | s.LogFields(fields...) 93 | } 94 | 95 | // LogInvokerSuccess logs information about a successful command response to s. 96 | func LogInvokerSuccess(s opentracing.Span, p *rinq.Payload) { 97 | s.LogFields( 98 | successEvent, 99 | log.Int("size", p.Len()), 100 | ) 101 | } 102 | 103 | // LogInvokerError logs information about err to s. 104 | func LogInvokerError(s opentracing.Span, err error) { 105 | ext.Error.Set(s, true) 106 | 107 | switch e := err.(type) { 108 | case rinq.Failure: 109 | s.LogFields( 110 | invokerFailureEvent, 111 | log.String("error.kind", e.Type), 112 | log.String("message", e.Message), 113 | invokerErrorSourceServer, 114 | log.Int("size", e.Payload.Len()), 115 | ) 116 | 117 | case rinq.CommandError: 118 | s.LogFields( 119 | errorEvent, 120 | log.String("message", e.Error()), 121 | invokerErrorSourceServer, 122 | ) 123 | 124 | default: 125 | s.LogFields( 126 | errorEvent, 127 | log.String("message", e.Error()), 128 | invokerErrorSourceClient, 129 | ) 130 | } 131 | } 132 | 133 | // LogServerRequest logs information about an incoming command request to s. 134 | func LogServerRequest(s opentracing.Span, peerID ident.PeerID, p *rinq.Payload) { 135 | s.LogFields( 136 | serverRequestEvent, 137 | log.String("server", peerID.String()), 138 | log.Int("size", p.Len()), 139 | ) 140 | } 141 | 142 | // LogServerSuccess logs information about a successful command response to s. 143 | func LogServerSuccess(s opentracing.Span, p *rinq.Payload) { 144 | s.LogFields( 145 | serverResponseEvent, 146 | log.Int("size", p.Len()), 147 | ) 148 | } 149 | 150 | // LogServerError logs information about err to s. 151 | func LogServerError(s opentracing.Span, err error) { 152 | switch e := err.(type) { 153 | case rinq.Failure: 154 | s.LogFields( 155 | serverResponseEvent, 156 | log.String("error.kind", e.Type), 157 | log.String("message", e.Message), 158 | log.Int("size", e.Payload.Len()), 159 | ) 160 | 161 | default: 162 | ext.Error.Set(s, true) 163 | 164 | s.LogFields( 165 | serverResponseEvent, 166 | log.String("message", e.Error()), 167 | ) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/internal/opentr/common.go: -------------------------------------------------------------------------------- 1 | package opentr 2 | 3 | import ( 4 | opentracing "github.com/opentracing/opentracing-go" 5 | "github.com/opentracing/opentracing-go/log" 6 | ) 7 | 8 | var ( 9 | successEvent = log.String("event", "success") 10 | errorEvent = log.String("event", "error") 11 | ) 12 | 13 | // AddTraceID configures span s to have traceID set to the given id. 14 | func AddTraceID(s opentracing.Span, id string) { 15 | if id != "" { 16 | s.SetTag("traceID", id) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/internal/opentr/common_test.go: -------------------------------------------------------------------------------- 1 | package opentr_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | "github.com/rinq/rinq-go/src/internal/opentr" 7 | ) 8 | 9 | var _ = Describe("AddTraceID", func() { 10 | It("adds the traceID tag to the span", func() { 11 | span := &mockSpan{} 12 | 13 | opentr.AddTraceID(span, "") 14 | 15 | Expect(span.tags).Should(HaveKeyWithValue("traceID", "")) 16 | }) 17 | 18 | It("does not add the traceID tag to the span if the id is empty", func() { 19 | span := &mockSpan{} 20 | 21 | opentr.AddTraceID(span, "") 22 | 23 | Expect(span.tags).ShouldNot(HaveKey("traceID")) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/internal/opentr/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package opentr_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "opentr") 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/opentr/lazy_string.go: -------------------------------------------------------------------------------- 1 | package opentr 2 | 3 | import "github.com/opentracing/opentracing-go/log" 4 | 5 | func lazyString(key string, s func() string) log.Field { 6 | return log.Lazy(func(e log.Encoder) { 7 | e.EmitString(key, s()) 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/internal/opentr/mockspan_test.go: -------------------------------------------------------------------------------- 1 | package opentr_test 2 | 3 | import ( 4 | opentracing "github.com/opentracing/opentracing-go" 5 | "github.com/opentracing/opentracing-go/log" 6 | ) 7 | 8 | type mockSpan struct { 9 | opentracing.Span 10 | 11 | operationName string 12 | log []map[string]interface{} 13 | tags map[string]interface{} 14 | } 15 | 16 | // Sets or changes the operation name. 17 | func (s *mockSpan) SetOperationName(operationName string) opentracing.Span { 18 | s.operationName = operationName 19 | return s 20 | } 21 | 22 | // Adds a tag to the span. 23 | // 24 | // If there is a pre-existing tag set for `key`, it is overwritten. 25 | // 26 | // Tag values can be numeric types, strings, or bools. The behavior of 27 | // other tag value types is undefined at the OpenTracing level. If a 28 | // tracing system does not know how to handle a particular value type, it 29 | // may ignore the tag, but shall not panic. 30 | func (s *mockSpan) SetTag(key string, value interface{}) opentracing.Span { 31 | if s.tags == nil { 32 | s.tags = map[string]interface{}{} 33 | } 34 | 35 | s.tags[key] = value 36 | 37 | return s 38 | } 39 | 40 | // LogFields is an efficient and type-checked way to record key:value 41 | // logging data about a Span, though the programming interface is a little 42 | // more verbose than LogKV(). Here's an example: 43 | // 44 | // span.LogFields( 45 | // log.String("event", "soft error"), 46 | // log.String("type", "cache timeout"), 47 | // log.Int("waited.millis", 1500)) 48 | // 49 | // Also see Span.FinishWithOptions() and FinishOptions.BulkLogData. 50 | func (s *mockSpan) LogFields(fields ...log.Field) { 51 | m := map[string]interface{}{} 52 | e := &encoder{m} 53 | 54 | for _, f := range fields { 55 | f.Marshal(e) 56 | } 57 | 58 | s.log = append(s.log, m) 59 | } 60 | 61 | type encoder struct { 62 | m map[string]interface{} 63 | } 64 | 65 | func (e *encoder) EmitString(key, value string) { e.m[key] = value } 66 | func (e *encoder) EmitBool(key string, value bool) { e.m[key] = value } 67 | func (e *encoder) EmitInt(key string, value int) { e.m[key] = value } 68 | func (e *encoder) EmitInt32(key string, value int32) { e.m[key] = value } 69 | func (e *encoder) EmitInt64(key string, value int64) { e.m[key] = value } 70 | func (e *encoder) EmitUint32(key string, value uint32) { e.m[key] = value } 71 | func (e *encoder) EmitUint64(key string, value uint64) { e.m[key] = value } 72 | func (e *encoder) EmitFloat32(key string, value float32) { e.m[key] = value } 73 | func (e *encoder) EmitFloat64(key string, value float64) { e.m[key] = value } 74 | func (e *encoder) EmitObject(key string, value interface{}) { e.m[key] = value } 75 | func (e *encoder) EmitLazyLogger(value log.LazyLogger) { value(e) } 76 | -------------------------------------------------------------------------------- /src/internal/opentr/notify.go: -------------------------------------------------------------------------------- 1 | package opentr 2 | 3 | import ( 4 | opentracing "github.com/opentracing/opentracing-go" 5 | "github.com/opentracing/opentracing-go/ext" 6 | "github.com/opentracing/opentracing-go/log" 7 | "github.com/rinq/rinq-go/src/internal/attributes" 8 | "github.com/rinq/rinq-go/src/rinq" 9 | "github.com/rinq/rinq-go/src/rinq/constraint" 10 | "github.com/rinq/rinq-go/src/rinq/ident" 11 | ) 12 | 13 | var ( 14 | notifierUnicastEvent = log.String("event", "notify") 15 | notifierMulticastEvent = log.String("event", "notify-many") 16 | listenerReceiveEvent = log.String("event", "notification") 17 | ) 18 | 19 | // SetupNotification configures span as a command-related span. 20 | func SetupNotification( 21 | s opentracing.Span, 22 | id ident.MessageID, 23 | ns string, 24 | t string, 25 | ) { 26 | s.SetOperationName(ns + "::" + t + " notification") 27 | 28 | s.SetTag("subsystem", "notify") 29 | s.SetTag("message_id", id.String()) 30 | s.SetTag("namespace", ns) 31 | s.SetTag("type", t) 32 | } 33 | 34 | // LogNotifierUnicast logs information about a unicast notification to s. 35 | func LogNotifierUnicast( 36 | s opentracing.Span, 37 | attrs attributes.Catalog, 38 | target ident.SessionID, 39 | p *rinq.Payload, 40 | ) { 41 | fields := []log.Field{ 42 | notifierUnicastEvent, 43 | log.String("target", target.String()), 44 | log.Int("size", p.Len()), 45 | } 46 | 47 | if len(attrs) > 0 { 48 | fields = append(fields, lazyString("attributes", attrs.String)) 49 | } 50 | 51 | s.LogFields(fields...) 52 | } 53 | 54 | // LogNotifierMulticast logs informatin about a multicast notification to s. 55 | func LogNotifierMulticast( 56 | s opentracing.Span, 57 | attrs attributes.Catalog, 58 | con constraint.Constraint, 59 | p *rinq.Payload, 60 | ) { 61 | fields := []log.Field{ 62 | notifierMulticastEvent, 63 | log.String("constraint", con.String()), 64 | log.Int("size", p.Len()), 65 | } 66 | 67 | if len(attrs) > 0 { 68 | fields = append(fields, lazyString("attributes", attrs.String)) 69 | } 70 | 71 | s.LogFields(fields...) 72 | } 73 | 74 | // LogNotifierError logs information about err to s. 75 | func LogNotifierError(s opentracing.Span, err error) { 76 | ext.Error.Set(s, true) 77 | 78 | s.LogFields( 79 | errorEvent, 80 | log.String("message", err.Error()), 81 | ) 82 | } 83 | 84 | // LogListenerReceived logs information about a received notification to s. 85 | func LogListenerReceived(s opentracing.Span, ref ident.Ref, n rinq.Notification) { 86 | fields := []log.Field{ 87 | listenerReceiveEvent, 88 | log.String("recipient", ref.String()), 89 | log.Bool("multicast", n.IsMulticast), 90 | log.Int("size", n.Payload.Len()), 91 | } 92 | 93 | if n.IsMulticast { 94 | fields = append( 95 | fields, 96 | log.String("constraint", n.Constraint.String()), 97 | ) 98 | } 99 | 100 | s.LogFields(fields...) 101 | } 102 | -------------------------------------------------------------------------------- /src/internal/opentr/pkg.go: -------------------------------------------------------------------------------- 1 | // Package opentr provides convenience functions for setting up OpenTracing 2 | // spans to represent Rinq operations. 3 | package opentr 4 | -------------------------------------------------------------------------------- /src/internal/opentr/span.go: -------------------------------------------------------------------------------- 1 | package opentr 2 | 3 | import ( 4 | "context" 5 | 6 | opentracing "github.com/opentracing/opentracing-go" 7 | "github.com/opentracing/opentracing-go/ext" 8 | "github.com/rinq/rinq-go/src/rinq" 9 | ) 10 | 11 | // CommonSpanOptions contains span options that should be applied to any 12 | // spans started by Rinq. 13 | var CommonSpanOptions = []opentracing.StartSpanOption{ 14 | opentracing.Tag{ 15 | Key: string(ext.Component), 16 | Value: "rinq-go/" + rinq.Version, 17 | }, 18 | } 19 | 20 | // FollowsFrom creates a new span that with a follows-from relationship to the 21 | // span in ctx, if any. 22 | func FollowsFrom( 23 | ctx context.Context, 24 | tracer opentracing.Tracer, 25 | opts ...opentracing.StartSpanOption, 26 | ) (opentracing.Span, context.Context) { 27 | return childSpanFromContext( 28 | ctx, 29 | tracer, 30 | opentracing.FollowsFrom, 31 | opts..., 32 | ) 33 | } 34 | 35 | // ChildOf creates a new span that with a child-of relationship to the span in, 36 | // if any. 37 | func ChildOf( 38 | ctx context.Context, 39 | tracer opentracing.Tracer, 40 | opts ...opentracing.StartSpanOption, 41 | ) (opentracing.Span, context.Context) { 42 | return childSpanFromContext( 43 | ctx, 44 | tracer, 45 | opentracing.FollowsFrom, 46 | opts..., 47 | ) 48 | } 49 | 50 | func childSpanFromContext( 51 | ctx context.Context, 52 | tracer opentracing.Tracer, 53 | rel func(opentracing.SpanContext) opentracing.SpanReference, 54 | opts ...opentracing.StartSpanOption, 55 | ) (opentracing.Span, context.Context) { 56 | parent := opentracing.SpanFromContext(ctx) 57 | 58 | if parent != nil { 59 | opts = append(opts, rel(parent.Context())) 60 | tracer = parent.Tracer() 61 | } 62 | 63 | opts = append(opts, CommonSpanOptions...) 64 | 65 | span := tracer.StartSpan("", opts...) 66 | 67 | return span, opentracing.ContextWithSpan(ctx, span) 68 | } 69 | -------------------------------------------------------------------------------- /src/internal/remotesession/cache.go: -------------------------------------------------------------------------------- 1 | package remotesession 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/internal/attributes" 5 | "github.com/rinq/rinq-go/src/rinq/ident" 6 | ) 7 | 8 | // attrTableCache is a namespaced local cache of session attributes. 9 | type attrTableCache map[string]attrNamespaceCache 10 | 11 | // attrNamespaceCache is an entry in an attrTableCache, representing a single namespace. 12 | type attrNamespaceCache map[string]cachedAttr 13 | 14 | // cachedAttr is an entry in a attrNamespaceCache, representing a single attribute. 15 | type cachedAttr struct { 16 | Attr attributes.VAttr 17 | FetchedAt ident.Revision 18 | } 19 | -------------------------------------------------------------------------------- /src/internal/remotesession/client_logging.go: -------------------------------------------------------------------------------- 1 | package remotesession 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmalloc/twelf/src/twelf" 7 | "github.com/rinq/rinq-go/src/internal/attributes" 8 | "github.com/rinq/rinq-go/src/rinq/ident" 9 | "github.com/rinq/rinq-go/src/rinq/trace" 10 | ) 11 | 12 | func logUpdate( 13 | ctx context.Context, 14 | logger twelf.Logger, 15 | peerID ident.PeerID, 16 | ref ident.Ref, 17 | diff *attributes.Diff, 18 | ) { 19 | logger.Log( 20 | "%s updated remote session %s %s [%s]", 21 | peerID.ShortString(), 22 | ref.ShortString(), 23 | diff, 24 | trace.Get(ctx), 25 | ) 26 | } 27 | 28 | func logClear( 29 | ctx context.Context, 30 | logger twelf.Logger, 31 | peerID ident.PeerID, 32 | ref ident.Ref, 33 | ns string, 34 | ) { 35 | logger.Log( 36 | "%s cleared remote session %s %s::{*} [%s]", 37 | peerID.ShortString(), 38 | ref.ShortString(), 39 | trace.Get(ctx), 40 | ns, 41 | ) 42 | } 43 | 44 | func logClose( 45 | ctx context.Context, 46 | logger twelf.Logger, 47 | peerID ident.PeerID, 48 | ref ident.Ref, 49 | ) { 50 | logger.Log( 51 | "%s destroyed remote session %s [%s]", 52 | peerID.ShortString(), 53 | ref.ShortString(), 54 | trace.Get(ctx), 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/internal/remotesession/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package remotesession_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "remotesession") 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/remotesession/logging.go: -------------------------------------------------------------------------------- 1 | package remotesession 2 | 3 | import ( 4 | "github.com/jmalloc/twelf/src/twelf" 5 | "github.com/rinq/rinq-go/src/rinq/ident" 6 | ) 7 | 8 | func logCacheAdd( 9 | logger twelf.Logger, 10 | peerID ident.PeerID, 11 | sessID ident.SessionID, 12 | ) { 13 | logger.Debug( 14 | "%s discovered remote session %s ", 15 | peerID.ShortString(), 16 | sessID.ShortString(), 17 | ) 18 | } 19 | 20 | func logCacheMark( 21 | logger twelf.Logger, 22 | peerID ident.PeerID, 23 | sessID ident.SessionID, 24 | ) { 25 | logger.Debug( 26 | "%s marked remote session %s for removal from the store", 27 | peerID.ShortString(), 28 | sessID.ShortString(), 29 | ) 30 | } 31 | 32 | func logCacheRemove( 33 | logger twelf.Logger, 34 | peerID ident.PeerID, 35 | sessID ident.SessionID, 36 | ) { 37 | logger.Debug( 38 | "%s removed remote session %s from the store", 39 | peerID.ShortString(), 40 | sessID.ShortString(), 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/internal/remotesession/revision.go: -------------------------------------------------------------------------------- 1 | package remotesession 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rinq/rinq-go/src/internal/attributes" 7 | "github.com/rinq/rinq-go/src/internal/namespaces" 8 | "github.com/rinq/rinq-go/src/internal/revisions" 9 | "github.com/rinq/rinq-go/src/rinq" 10 | "github.com/rinq/rinq-go/src/rinq/ident" 11 | ) 12 | 13 | type revision struct { 14 | ref ident.Ref 15 | session *session 16 | } 17 | 18 | func (r *revision) SessionID() ident.SessionID { 19 | return r.ref.ID 20 | } 21 | 22 | func (r *revision) Refresh(ctx context.Context) (rinq.Revision, error) { 23 | rev, err := r.session.Head(ctx) 24 | 25 | if rinq.IsNotFound(err) { 26 | return revisions.Closed(r.ref.ID), nil 27 | } 28 | 29 | return rev, err 30 | } 31 | 32 | func (r *revision) Get(ctx context.Context, ns, key string) (rinq.Attr, error) { 33 | namespaces.MustValidate(ns) 34 | 35 | if r.ref.Rev == 0 { 36 | return rinq.Attr{Key: key}, nil 37 | } 38 | 39 | attrs, err := r.session.Fetch(ctx, r.ref.Rev, ns, key) 40 | if err != nil { 41 | return rinq.Attr{}, err 42 | } else if len(attrs) == 0 { 43 | return rinq.Attr{Key: key}, nil 44 | } 45 | 46 | return attrs[0], nil 47 | } 48 | 49 | func (r *revision) GetMany(ctx context.Context, ns string, keys ...string) (rinq.AttrTable, error) { 50 | namespaces.MustValidate(ns) 51 | 52 | table := attributes.Table{} 53 | 54 | for _, key := range keys { 55 | table[key] = rinq.Attr{Key: key} 56 | } 57 | 58 | if r.ref.Rev == 0 { 59 | return table, nil 60 | } 61 | 62 | attrs, err := r.session.Fetch(ctx, r.ref.Rev, ns, keys...) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | for _, attr := range attrs { 68 | table[attr.Key] = attr 69 | } 70 | 71 | return table, nil 72 | } 73 | 74 | func (r *revision) Update(ctx context.Context, ns string, attrs ...rinq.Attr) (rinq.Revision, error) { 75 | namespaces.MustValidate(ns) 76 | 77 | rev, err := r.session.TryUpdate(ctx, r.ref.Rev, ns, attrs) 78 | if err != nil { 79 | return r, err 80 | } 81 | 82 | return rev, nil 83 | } 84 | 85 | func (r *revision) Clear(ctx context.Context, ns string) (rinq.Revision, error) { 86 | namespaces.MustValidate(ns) 87 | 88 | rev, err := r.session.TryClear(ctx, r.ref.Rev, ns) 89 | if err != nil { 90 | return r, err 91 | } 92 | 93 | return rev, nil 94 | } 95 | 96 | func (r *revision) Destroy(ctx context.Context) error { 97 | return r.session.TryDestroy(ctx, r.ref.Rev) 98 | } 99 | -------------------------------------------------------------------------------- /src/internal/remotesession/server_logging.go: -------------------------------------------------------------------------------- 1 | package remotesession 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmalloc/twelf/src/twelf" 7 | "github.com/rinq/rinq-go/src/internal/attributes" 8 | "github.com/rinq/rinq-go/src/internal/localsession" 9 | "github.com/rinq/rinq-go/src/rinq/ident" 10 | "github.com/rinq/rinq-go/src/rinq/trace" 11 | ) 12 | 13 | func logRemoteUpdate( 14 | ctx context.Context, 15 | logger twelf.Logger, 16 | ref ident.Ref, 17 | peerID ident.PeerID, 18 | diff *attributes.Diff, 19 | ) { 20 | logger.Log( 21 | "%s session updated by %s %s [%s]", 22 | ref.ShortString(), 23 | peerID.ShortString(), 24 | diff, 25 | trace.Get(ctx), 26 | ) 27 | } 28 | 29 | func logRemoteClear( 30 | ctx context.Context, 31 | logger twelf.Logger, 32 | ref ident.Ref, 33 | peerID ident.PeerID, 34 | diff *attributes.Diff, 35 | ) { 36 | logger.Log( 37 | "%s session cleared by %s %s [%s]", 38 | ref.ShortString(), 39 | peerID.ShortString(), 40 | diff, 41 | trace.Get(ctx), 42 | ) 43 | } 44 | 45 | func logRemoteDestroy( 46 | ctx context.Context, 47 | logger twelf.Logger, 48 | sess *localsession.Session, 49 | peerID ident.PeerID, 50 | ) { 51 | ref, attrs := sess.Attrs() 52 | 53 | logger.Log( 54 | "%s session destroyed by %s %s [%s]", 55 | ref.ShortString(), 56 | peerID.ShortString(), 57 | attrs, 58 | trace.Get(ctx), 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/internal/remotesession/store.go: -------------------------------------------------------------------------------- 1 | package remotesession 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/jmalloc/twelf/src/twelf" 8 | opentracing "github.com/opentracing/opentracing-go" 9 | "github.com/rinq/rinq-go/src/internal/command" 10 | "github.com/rinq/rinq-go/src/internal/revisions" 11 | "github.com/rinq/rinq-go/src/internal/service" 12 | "github.com/rinq/rinq-go/src/rinq" 13 | "github.com/rinq/rinq-go/src/rinq/ident" 14 | ) 15 | 16 | // Store is a local cache of remote revisions. 17 | type Store interface { 18 | revisions.Store 19 | service.Service 20 | } 21 | 22 | type store struct { 23 | service.Service 24 | sm *service.StateMachine 25 | 26 | peerID ident.PeerID 27 | client *client 28 | interval time.Duration 29 | logger twelf.Logger 30 | 31 | mutex sync.Mutex 32 | cache map[ident.SessionID]*cacheEntry 33 | } 34 | 35 | // NewStore returns a new store for revisions of remote sessions. 36 | func NewStore( 37 | peerID ident.PeerID, 38 | invoker command.Invoker, 39 | pruneInterval time.Duration, 40 | logger twelf.Logger, 41 | tracer opentracing.Tracer, 42 | ) Store { 43 | s := &store{ 44 | peerID: peerID, 45 | client: newClient(peerID, invoker, logger, tracer), 46 | interval: pruneInterval, 47 | logger: logger, 48 | cache: map[ident.SessionID]*cacheEntry{}, 49 | } 50 | 51 | s.sm = service.NewStateMachine(s.run, nil) 52 | s.Service = s.sm 53 | 54 | go s.sm.Run() 55 | 56 | return s 57 | } 58 | 59 | type cacheEntry struct { 60 | Session *session 61 | Marked bool 62 | } 63 | 64 | func (s *store) GetRevision(ref ident.Ref) (rinq.Revision, error) { 65 | sess := s.getSession(ref.ID) 66 | return sess.At(ref.Rev), nil 67 | } 68 | 69 | func (s *store) getSession(id ident.SessionID) *session { 70 | s.mutex.Lock() 71 | defer s.mutex.Unlock() 72 | 73 | if entry, ok := s.cache[id]; ok { 74 | entry.Marked = false 75 | return entry.Session 76 | } 77 | 78 | sess := newSession(id, s.client) 79 | s.cache[id] = &cacheEntry{sess, false} 80 | logCacheAdd(s.logger, s.peerID, id) 81 | 82 | return sess 83 | } 84 | 85 | func (s *store) run() (service.State, error) { 86 | for { 87 | select { 88 | case <-time.After(s.interval): 89 | s.prune() 90 | 91 | case <-s.sm.Graceful: 92 | return nil, nil 93 | 94 | case <-s.sm.Forceful: 95 | return nil, nil 96 | } 97 | } 98 | } 99 | 100 | func (s *store) prune() { 101 | s.mutex.Lock() 102 | defer s.mutex.Unlock() 103 | 104 | for id, entry := range s.cache { 105 | if entry.Marked { 106 | delete(s.cache, id) 107 | logCacheRemove(s.logger, s.peerID, id) 108 | } else { 109 | entry.Marked = true 110 | logCacheMark(s.logger, s.peerID, id) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/internal/remotesession/types.go: -------------------------------------------------------------------------------- 1 | package remotesession 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/internal/attributes" 5 | "github.com/rinq/rinq-go/src/rinq" 6 | "github.com/rinq/rinq-go/src/rinq/ident" 7 | ) 8 | 9 | const ( 10 | sessionNamespace = "_sess" 11 | ) 12 | 13 | const ( 14 | fetchCommand = "fetch" 15 | updateCommand = "update" 16 | clearCommand = "clear" 17 | destroyCommand = "destroy" 18 | ) 19 | 20 | type fetchRequest struct { 21 | Seq uint32 `json:"s"` 22 | Namespace string `json:"ns,omitempty"` 23 | Keys []string `json:"k,omitempty"` 24 | } 25 | 26 | type fetchResponse struct { 27 | Rev ident.Revision `json:"r"` 28 | Attrs attributes.VList `json:"a,omitempty"` 29 | } 30 | 31 | type updateRequest struct { 32 | Seq uint32 `json:"s"` 33 | Rev ident.Revision `json:"r"` 34 | Namespace string `json:"ns"` 35 | Attrs attributes.List `json:"a,omitempty"` // omitted for "clear" command 36 | } 37 | 38 | type updateResponse struct { 39 | Rev ident.Revision `json:"r"` 40 | CreatedRevs []ident.Revision `json:"cr,omitempty"` 41 | } 42 | 43 | type destroyRequest struct { 44 | Seq uint32 `json:"s"` 45 | Rev ident.Revision `json:"r"` 46 | } 47 | 48 | const ( 49 | notFoundFailure = "not-found" 50 | staleUpdateFailure = "stale" 51 | frozenAttributesFailure = "frozen" 52 | ) 53 | 54 | // errorToFailure returns the appropriate failure type based on the type of err. 55 | func errorToFailure(err error) error { 56 | switch err.(type) { 57 | case rinq.NotFoundError: 58 | return rinq.Failure{Type: notFoundFailure} 59 | case rinq.StaleUpdateError: 60 | return rinq.Failure{Type: staleUpdateFailure} 61 | case rinq.FrozenAttributesError: 62 | return rinq.Failure{Type: frozenAttributesFailure} 63 | default: 64 | return err 65 | } 66 | } 67 | 68 | // failureToError returns the appropriate error based on the failure type of err. 69 | func failureToError(ref ident.Ref, err error) error { 70 | switch rinq.FailureType(err) { 71 | case notFoundFailure: 72 | return rinq.NotFoundError{ID: ref.ID} 73 | case staleUpdateFailure: 74 | return rinq.StaleUpdateError{Ref: ref} 75 | case frozenAttributesFailure: 76 | return rinq.FrozenAttributesError{Ref: ref} 77 | } 78 | 79 | return err 80 | } 81 | -------------------------------------------------------------------------------- /src/internal/revisions/closed.go: -------------------------------------------------------------------------------- 1 | package revisions 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rinq/rinq-go/src/rinq" 7 | "github.com/rinq/rinq-go/src/rinq/ident" 8 | ) 9 | 10 | // Closed returns an revision that behaves as though its session has been closed. 11 | func Closed(id ident.SessionID) rinq.Revision { 12 | return closed(id) 13 | } 14 | 15 | type closed ident.SessionID 16 | 17 | func (r closed) SessionID() ident.SessionID { 18 | return ident.SessionID(r) 19 | } 20 | 21 | func (r closed) Refresh(context.Context) (rinq.Revision, error) { 22 | return r, nil 23 | } 24 | 25 | func (r closed) Get(context.Context, string, string) (rinq.Attr, error) { 26 | return rinq.Attr{}, rinq.NotFoundError{ID: ident.SessionID(r)} 27 | } 28 | 29 | func (r closed) GetMany(context.Context, string, ...string) (rinq.AttrTable, error) { 30 | return nil, rinq.NotFoundError{ID: ident.SessionID(r)} 31 | } 32 | 33 | func (r closed) Update(context.Context, string, ...rinq.Attr) (rinq.Revision, error) { 34 | return r, rinq.NotFoundError{ID: ident.SessionID(r)} 35 | } 36 | 37 | func (r closed) Clear(context.Context, string) (rinq.Revision, error) { 38 | return r, rinq.NotFoundError{ID: ident.SessionID(r)} 39 | } 40 | 41 | func (r closed) Destroy(context.Context) error { 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /src/internal/revisions/store.go: -------------------------------------------------------------------------------- 1 | package revisions 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/rinq" 5 | "github.com/rinq/rinq-go/src/rinq/ident" 6 | ) 7 | 8 | // Store is an interface for retrieving session revisions. 9 | type Store interface { 10 | // GetRevision returns the session revision for the given ref. 11 | GetRevision(ident.Ref) (rinq.Revision, error) 12 | } 13 | 14 | // AggregateStore is a revision store that forwards to one of two other stores 15 | // based on whether the requested revision is considered "local" or "remote". 16 | type AggregateStore struct { 17 | PeerID ident.PeerID 18 | Local Store 19 | Remote Store 20 | } 21 | 22 | // NewAggregateStore returns a new store that attempts operations first on the 23 | // local store, then on the remote store. 24 | func NewAggregateStore( 25 | peerID ident.PeerID, 26 | local Store, 27 | remote Store, 28 | ) *AggregateStore { 29 | return &AggregateStore{ 30 | peerID, 31 | local, 32 | remote, 33 | } 34 | } 35 | 36 | // GetRevision returns the session revision for the given ref. 37 | func (s *AggregateStore) GetRevision(ref ident.Ref) (rinq.Revision, error) { 38 | if ref.ID.Peer == s.PeerID { 39 | if s.Local != nil { 40 | return s.Local.GetRevision(ref) 41 | } 42 | } else if s.Remote != nil { 43 | return s.Remote.GetRevision(ref) 44 | } 45 | 46 | return Closed(ref.ID), nil 47 | } 48 | -------------------------------------------------------------------------------- /src/internal/service/error.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "errors" 4 | 5 | // ErrStopped is returned by any operation that can not be fulfilled because 6 | // the service that provides it is stopping or has already stopped. 7 | var ErrStopped = errors.New("service has been stopped") 8 | -------------------------------------------------------------------------------- /src/internal/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "sync" 4 | 5 | // Service is an interface for background tasks that can finish with an error. 6 | type Service interface { 7 | // Done returns a channel that is closed when the service is stopped. 8 | Done() <-chan struct{} 9 | 10 | // Err returns the error that caused the Done() channel to close, if any. 11 | Err() error 12 | 13 | // Stop halts the service immediately. 14 | Stop() 15 | 16 | // GracefulStop() halts the service once it has finished any pending work. 17 | GracefulStop() 18 | } 19 | 20 | // WaitAll returns a channel that is closed when all of the given services are 21 | // done. 22 | func WaitAll(services ...Service) <-chan struct{} { 23 | var wg sync.WaitGroup 24 | 25 | for _, s := range services { 26 | wg.Add(1) 27 | go func(s Service) { 28 | for range s.Done() { 29 | } 30 | wg.Done() 31 | }(s) 32 | } 33 | 34 | done := make(chan struct{}) 35 | go func() { 36 | wg.Wait() 37 | close(done) 38 | }() 39 | 40 | return done 41 | } 42 | -------------------------------------------------------------------------------- /src/internal/service/statemachine.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "sync" 4 | 5 | // State is a handler for a particular application state. 6 | // 7 | // The function should block until a state transition is necessary. 8 | // 9 | // If next is nil, the service stops such that the Done() channel is closed and 10 | // s.Err() returns err, which may be nil. 11 | // 12 | // Otherwise, next is called and the process is repeated. 13 | type State func() (next State, err error) 14 | 15 | // Finalizer is called when the state machine stops. 16 | type Finalizer func(error) error 17 | 18 | // StateMachine is a state-machine based implementation of the Service interface. 19 | type StateMachine struct { 20 | Forceful chan struct{} 21 | Graceful chan struct{} 22 | Finalized chan struct{} 23 | Commands chan request 24 | 25 | state State 26 | finalizer Finalizer 27 | 28 | mutex sync.RWMutex 29 | err error 30 | } 31 | 32 | // NewStateMachine returns a new service trait. 33 | func NewStateMachine( 34 | s State, 35 | f Finalizer, 36 | ) *StateMachine { 37 | return &StateMachine{ 38 | Forceful: make(chan struct{}), 39 | Graceful: make(chan struct{}), 40 | Finalized: make(chan struct{}), 41 | 42 | state: s, 43 | Commands: make(chan request), 44 | finalizer: f, 45 | } 46 | } 47 | 48 | // Run enters the initial state and runs until the service stops. 49 | func (s *StateMachine) Run() { 50 | var err error 51 | 52 | for s.state != nil && err == nil { 53 | s.state, err = s.state() 54 | } 55 | 56 | if s.finalizer != nil { 57 | err = s.finalizer(err) 58 | } 59 | 60 | s.mutex.Lock() 61 | s.err = err 62 | s.mutex.Unlock() 63 | 64 | s.close() 65 | } 66 | 67 | // Done returns a channel that is closed when the service is stopped. 68 | func (s *StateMachine) Done() <-chan struct{} { 69 | return s.Finalized 70 | } 71 | 72 | // Err returns the error that caused the Done() channel to close, if any. 73 | func (s *StateMachine) Err() error { 74 | s.mutex.RLock() 75 | defer s.mutex.RUnlock() 76 | return s.err 77 | } 78 | 79 | // Stop halts the service immediately. 80 | func (s *StateMachine) Stop() { 81 | s.mutex.Lock() 82 | defer s.mutex.Unlock() 83 | 84 | select { 85 | case <-s.Forceful: 86 | return 87 | default: 88 | close(s.Forceful) 89 | } 90 | } 91 | 92 | // GracefulStop halts the service once it has finished any pending work. 93 | func (s *StateMachine) GracefulStop() { 94 | s.mutex.Lock() 95 | defer s.mutex.Unlock() 96 | 97 | select { 98 | case <-s.Forceful: 99 | return 100 | case <-s.Graceful: 101 | return 102 | default: 103 | close(s.Graceful) 104 | } 105 | } 106 | 107 | type request struct { 108 | fn func() error 109 | reply chan<- error 110 | } 111 | 112 | // Do enqueues fn in the command channel to be processed by the state-machine. 113 | // It returns ErrStopped if the state-machine is stopping or has already stopped. 114 | func (s *StateMachine) Do(fn func() error) error { 115 | reply := make(chan error, 1) 116 | req := request{fn, reply} 117 | 118 | select { 119 | case s.Commands <- req: 120 | case <-s.Graceful: 121 | return ErrStopped 122 | case <-s.Forceful: 123 | return ErrStopped 124 | case <-s.Finalized: 125 | return ErrStopped 126 | } 127 | 128 | select { 129 | case err := <-reply: 130 | return err 131 | case <-s.Graceful: 132 | return ErrStopped 133 | case <-s.Forceful: 134 | return ErrStopped 135 | case <-s.Finalized: 136 | return ErrStopped 137 | } 138 | } 139 | 140 | // DoGraceful enqueues fn in the command channel to be processed by the 141 | // state-machine. It differs from s.Call() in that it still enqueues the command 142 | // if the state-machine is stopping gracefully (but not if it has been stopped) 143 | // forcefully. 144 | func (s *StateMachine) DoGraceful(fn func() error) error { 145 | reply := make(chan error, 1) 146 | req := request{fn, reply} 147 | 148 | select { 149 | case s.Commands <- req: 150 | case <-s.Forceful: 151 | return ErrStopped 152 | case <-s.Finalized: 153 | return ErrStopped 154 | } 155 | 156 | select { 157 | case err := <-reply: 158 | return err 159 | case <-s.Forceful: 160 | return ErrStopped 161 | case <-s.Finalized: 162 | return ErrStopped 163 | } 164 | } 165 | 166 | // Execute handles a command request. 167 | func (s *StateMachine) Execute(req request) { 168 | req.reply <- req.fn() 169 | } 170 | 171 | func (s *StateMachine) close() { 172 | // protect against panic() that could occur when closing already closed 173 | // channels if s.close() were to be called concurrently. 174 | s.mutex.Lock() 175 | defer s.mutex.Unlock() 176 | 177 | select { 178 | case <-s.Finalized: 179 | return 180 | default: 181 | } 182 | 183 | close(s.Finalized) 184 | 185 | select { 186 | case <-s.Forceful: 187 | default: 188 | close(s.Forceful) 189 | } 190 | 191 | select { 192 | case <-s.Graceful: 193 | default: 194 | close(s.Graceful) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/internal/x/bufferpool/bufferpool.go: -------------------------------------------------------------------------------- 1 | package bufferpool 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | var buffers sync.Pool 9 | 10 | // Get fetches a buffer from the buffer pool. 11 | func Get() *bytes.Buffer { 12 | return buffers.Get().(*bytes.Buffer) 13 | } 14 | 15 | // Put returns a buffer to the buffer pool. 16 | func Put(buf *bytes.Buffer) { 17 | if buf != nil { 18 | buf.Reset() 19 | buffers.Put(buf) 20 | } 21 | } 22 | 23 | func init() { 24 | buffers.New = func() interface{} { 25 | return &bytes.Buffer{} 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/internal/x/bufferpool/bufferpool_test.go: -------------------------------------------------------------------------------- 1 | package bufferpool_test 2 | 3 | import ( 4 | "bytes" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | . "github.com/rinq/rinq-go/src/internal/x/bufferpool" 9 | ) 10 | 11 | var _ = Describe("Get", func() { 12 | It("returns a bytes.Buffer pointer", func() { 13 | buffer := Get() 14 | Expect(buffer).ShouldNot(BeNil()) 15 | }) 16 | 17 | It("recycles buffers", func() { 18 | buffer := Get() 19 | Put(buffer) 20 | 21 | Expect(Get()).To(Equal(buffer)) 22 | }) 23 | }) 24 | 25 | var _ = Describe("Put", func() { 26 | It("accepts a buffer pointer", func() { 27 | var buffer bytes.Buffer 28 | Put(&buffer) 29 | }) 30 | 31 | It("accepts a nil pointer", func() { 32 | var buffer *bytes.Buffer 33 | Put(buffer) 34 | 35 | Expect(Get()).ShouldNot(BeNil()) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/internal/x/bufferpool/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package bufferpool_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "bufferpool") 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/x/cbor/cbor.go: -------------------------------------------------------------------------------- 1 | package cbor 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "sync" 7 | 8 | "github.com/ugorji/go/codec" 9 | ) 10 | 11 | // Nil is the nil value encoded in CBOR 12 | var Nil []byte 13 | 14 | var encoders sync.Pool 15 | var decoders sync.Pool 16 | 17 | // Encode writes v to w in CBOR format. 18 | func Encode(w io.Writer, v interface{}) error { 19 | e := encoders.Get().(*codec.Encoder) 20 | defer encoders.Put(e) 21 | 22 | e.Reset(w) 23 | return e.Encode(v) 24 | } 25 | 26 | // MustEncode writes v to w in CBOR format, or panics if unable to do so. 27 | func MustEncode(w io.Writer, v interface{}) { 28 | e := encoders.Get().(*codec.Encoder) 29 | defer encoders.Put(e) 30 | 31 | e.Reset(w) 32 | e.MustEncode(v) 33 | } 34 | 35 | // Decode reads CBOR data from r and unpacks into v. 36 | func Decode(r io.Reader, v interface{}) error { 37 | d := decoders.Get().(*codec.Decoder) 38 | defer decoders.Put(d) 39 | 40 | d.Reset(r) 41 | return d.Decode(v) 42 | } 43 | 44 | // MustDecode reads CBOR data from r and unpacks into v, or panics if unable to 45 | // do so. 46 | func MustDecode(r io.Reader, v interface{}) { 47 | d := decoders.Get().(*codec.Decoder) 48 | defer decoders.Put(d) 49 | 50 | d.Reset(r) 51 | d.MustDecode(v) 52 | } 53 | 54 | // DecodeBytes parses CBOR data in b and unpacks into v. 55 | func DecodeBytes(b []byte, v interface{}) error { 56 | d := decoders.Get().(*codec.Decoder) 57 | defer decoders.Put(d) 58 | 59 | d.ResetBytes(b) 60 | return d.Decode(v) 61 | } 62 | 63 | // MustDecodeBytes parses CBOR data in b and unpacks into v, or panics if unable 64 | // to do so. 65 | func MustDecodeBytes(b []byte, v interface{}) { 66 | d := decoders.Get().(*codec.Decoder) 67 | defer decoders.Put(d) 68 | 69 | d.ResetBytes(b) 70 | d.MustDecode(v) 71 | } 72 | 73 | func init() { 74 | var handle codec.CborHandle 75 | 76 | encoders.New = func() interface{} { 77 | return codec.NewEncoder(nil, &handle) 78 | } 79 | 80 | decoders.New = func() interface{} { 81 | return codec.NewDecoder(nil, &handle) 82 | } 83 | 84 | e := encoders.Get().(*codec.Encoder) 85 | defer encoders.Put(e) 86 | 87 | var buffer bytes.Buffer 88 | e.Reset(&buffer) 89 | e.MustEncode(nil) 90 | 91 | Nil = buffer.Bytes() 92 | } 93 | -------------------------------------------------------------------------------- /src/internal/x/cbor/cbor_test.go: -------------------------------------------------------------------------------- 1 | package cbor_test 2 | 3 | import ( 4 | "bytes" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | . "github.com/rinq/rinq-go/src/internal/x/cbor" 9 | ) 10 | 11 | var _ = Describe("Encode", func() { 12 | It("returns the expected binary representation", func() { 13 | var buf bytes.Buffer 14 | err := Encode(&buf, 123) 15 | 16 | Expect(err).ShouldNot(HaveOccurred()) 17 | Expect(buf.Bytes()).To(Equal([]byte{24, 123})) 18 | }) 19 | }) 20 | 21 | var _ = Describe("MustEncode", func() { 22 | It("returns the expected binary representation", func() { 23 | var buf bytes.Buffer 24 | MustEncode(&buf, 123) 25 | 26 | Expect(buf.Bytes()).To(Equal([]byte{24, 123})) 27 | }) 28 | }) 29 | 30 | var _ = Describe("Decode", func() { 31 | It("produces the expected value", func() { 32 | buf := bytes.NewBuffer([]byte{24, 123}) 33 | 34 | var v interface{} 35 | err := Decode(buf, &v) 36 | 37 | Expect(err).ShouldNot(HaveOccurred()) 38 | Expect(v).To(Equal(uint64(123))) 39 | }) 40 | }) 41 | 42 | var _ = Describe("MustDecode", func() { 43 | It("produces the expected value", func() { 44 | buf := bytes.NewBuffer([]byte{24, 123}) 45 | 46 | var v interface{} 47 | MustDecode(buf, &v) 48 | 49 | Expect(v).To(Equal(uint64(123))) 50 | }) 51 | }) 52 | 53 | var _ = Describe("DecodeBytes", func() { 54 | It("produces the expected value", func() { 55 | buf := []byte{24, 123} 56 | 57 | var v interface{} 58 | err := DecodeBytes(buf, &v) 59 | 60 | Expect(err).ShouldNot(HaveOccurred()) 61 | Expect(v).To(Equal(uint64(123))) 62 | }) 63 | }) 64 | 65 | var _ = Describe("MustDecodeBytes", func() { 66 | It("produces the expected value", func() { 67 | buf := []byte{24, 123} 68 | 69 | var v interface{} 70 | MustDecodeBytes(buf, &v) 71 | 72 | Expect(v).To(Equal(uint64(123))) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/internal/x/cbor/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package cbor_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "cbor") 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/x/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | // UInt parses and validates a non-zero integer from the environment 11 | // variable named v. 12 | func UInt(v string) (uint, bool, error) { 13 | if s := os.Getenv(v); s != "" { 14 | n, err := strconv.ParseUint(s, 10, 31) 15 | if err != nil || n == 0 { 16 | return 0, false, fmt.Errorf("%s must be a non-zero integer", v) 17 | } 18 | 19 | return uint(n), true, nil 20 | } 21 | 22 | return 0, false, nil 23 | } 24 | 25 | // Duration parses and validates a non-zero duration in milliseconds 26 | // from the environment variable named v. 27 | func Duration(v string) (time.Duration, bool, error) { 28 | if s := os.Getenv(v); s != "" { 29 | n, err := strconv.ParseUint(s, 10, 63) 30 | if err != nil { 31 | return 0, false, fmt.Errorf("%s must be a non-zero duration (in milliseconds)", v) 32 | } 33 | 34 | return time.Duration(n) * time.Millisecond, true, nil 35 | } 36 | 37 | return 0, false, nil 38 | } 39 | 40 | // Bool parses and validates a boolean string from the environment variable 41 | // named v. 42 | func Bool(v string) (bool, bool, error) { 43 | switch os.Getenv(v) { 44 | case "true": 45 | return true, true, nil 46 | case "false": 47 | return false, true, nil 48 | case "": 49 | return false, false, nil 50 | default: 51 | return false, false, fmt.Errorf("%s must be 'true' or 'false'", v) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/internal/x/repr/escape.go: -------------------------------------------------------------------------------- 1 | package repr 2 | 3 | import ( 4 | "encoding/json" 5 | "regexp" 6 | ) 7 | 8 | // Escape returns human-readable, possibly quoted, escaped representation of s. 9 | func Escape(s string) string { 10 | if pattern.MatchString(s) { 11 | return s 12 | } 13 | 14 | buf, err := json.Marshal(s) 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | return string(buf) 20 | } 21 | 22 | var pattern *regexp.Regexp 23 | 24 | func init() { 25 | pattern = regexp.MustCompile(`^[A-Za-z0-9_\.\-]+$`) 26 | } 27 | -------------------------------------------------------------------------------- /src/internal/x/repr/escape_test.go: -------------------------------------------------------------------------------- 1 | package repr_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/ginkgo/extensions/table" 6 | . "github.com/onsi/gomega" 7 | "github.com/rinq/rinq-go/src/internal/x/repr" 8 | ) 9 | 10 | var _ = Describe("Escape", func() { 11 | DescribeTable( 12 | "returns the expected representation", 13 | func(in, out string) { 14 | Expect(repr.Escape(in)).To(Equal(out)) 15 | }, 16 | Entry("empty", "", `""`), 17 | Entry("no special characters", "Aa0_-", `Aa0_-`), 18 | Entry("special characters", "foo bar", `"foo bar"`), 19 | ) 20 | }) 21 | -------------------------------------------------------------------------------- /src/internal/x/repr/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package repr_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "repr") 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/x/repr/pkg.go: -------------------------------------------------------------------------------- 1 | // Package repr contains utilities for building string representations of values. 2 | package repr 3 | -------------------------------------------------------------------------------- /src/internal/x/syncx/lock.go: -------------------------------------------------------------------------------- 1 | package syncx 2 | 3 | import "sync" 4 | 5 | // Lock acquires a lock and returns a function that releases the lock the first 6 | // time it is called. 7 | // 8 | // This can be be used to offer panic-safe mutex locking, but also unlock the 9 | // mutex before the end of the function if necessary. 10 | func Lock(l sync.Locker) func() { 11 | l.Lock() 12 | 13 | return func() { 14 | if l != nil { 15 | l.Unlock() 16 | l = nil 17 | } 18 | } 19 | } 20 | 21 | // RLock is a variant of Lock that operates on a read-write mutex's read locker. 22 | func RLock(m *sync.RWMutex) func() { 23 | return Lock(m.RLocker()) 24 | } 25 | -------------------------------------------------------------------------------- /src/internal/x/syncx/pkg.go: -------------------------------------------------------------------------------- 1 | // Package syncx contains utilities for working with synchronization primitives. 2 | package syncx 3 | -------------------------------------------------------------------------------- /src/rinq/attr.go: -------------------------------------------------------------------------------- 1 | package rinq 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/internal/x/bufferpool" 5 | "github.com/rinq/rinq-go/src/internal/x/repr" 6 | ) 7 | 8 | // Attr is a sesssion attribute. 9 | // 10 | // Sessions contain a versioned key/value store. See the Session interface for 11 | // more information. 12 | type Attr struct { 13 | // Key is an application-defined identifier for the attribute. Keys are 14 | // unique within a session. Any valid UTF-8 string can be used a key, 15 | // including the empty string. 16 | Key string `json:"k"` 17 | 18 | // Value is the attribute's value. Any valid UTF-8 string can be used as a 19 | // value, including the empty string. 20 | Value string `json:"v,omitempty"` 21 | 22 | // IsFrozen is true if the attribute is "frozen" such that it can never be 23 | // altered again (for a given session). 24 | IsFrozen bool `json:"f,omitempty"` 25 | } 26 | 27 | // Set is a convenience method that creates an Attr with the specified key and 28 | // value. 29 | func Set(key, value string) Attr { 30 | return Attr{Key: key, Value: value} 31 | } 32 | 33 | // Freeze is a convenience method that returns an Attr with the specified key 34 | // and value, and the IsFrozen flag set to true. 35 | func Freeze(key, value string) Attr { 36 | return Attr{Key: key, Value: value, IsFrozen: true} 37 | } 38 | 39 | func (attr Attr) String() string { 40 | buf := bufferpool.Get() 41 | defer bufferpool.Put(buf) 42 | 43 | if attr.Value == "" { 44 | if attr.IsFrozen { 45 | buf.WriteString("!") 46 | } else { 47 | buf.WriteString("-") 48 | } 49 | buf.WriteString(repr.Escape(attr.Key)) 50 | } else { 51 | buf.WriteString(repr.Escape(attr.Key)) 52 | if attr.IsFrozen { 53 | buf.WriteString("@") 54 | } else { 55 | buf.WriteString("=") 56 | } 57 | buf.WriteString(repr.Escape(attr.Value)) 58 | } 59 | 60 | return buf.String() 61 | } 62 | 63 | // AttrTable is a read-only table of session attributes. 64 | type AttrTable interface { 65 | // Get returns the attribute with key k. 66 | Get(k string) (Attr, bool) 67 | 68 | // Each calls fn for each attribute in the collection. Iteration stops 69 | // when fn returns false. 70 | Each(fn func(Attr) bool) 71 | 72 | // IsEmpty returns true if there are no attributes in the table. 73 | IsEmpty() bool 74 | 75 | String() string 76 | } 77 | -------------------------------------------------------------------------------- /src/rinq/attr_example_test.go: -------------------------------------------------------------------------------- 1 | // +build !without_amqp,!without_examples 2 | 3 | package rinq_test 4 | 5 | import ( 6 | "fmt" 7 | 8 | . "github.com/rinq/rinq-go/src/rinq" 9 | ) 10 | 11 | func ExampleSet() { 12 | attr := Set("foo", "bar") 13 | 14 | fmt.Println(attr) 15 | // Output: foo=bar 16 | } 17 | 18 | func ExampleFreeze() { 19 | attr := Freeze("foo", "bar") 20 | 21 | fmt.Println(attr) 22 | // Output: foo@bar 23 | } 24 | -------------------------------------------------------------------------------- /src/rinq/attr_test.go: -------------------------------------------------------------------------------- 1 | package rinq_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | "github.com/rinq/rinq-go/src/rinq" 7 | ) 8 | 9 | var _ = Describe("Attr", func() { 10 | Describe("String", func() { 11 | It("uses 'equals' syntax", func() { 12 | attr := rinq.Attr{Key: "foo", Value: "bar"} 13 | Expect(attr.String()).To(Equal("foo=bar")) 14 | }) 15 | 16 | It("uses 'at' syntax for frozen attributes", func() { 17 | attr := rinq.Attr{Key: "foo", Value: "bar", IsFrozen: true} 18 | Expect(attr.String()).To(Equal("foo@bar")) 19 | }) 20 | 21 | It("uses 'minus' syntax for empty attributes", func() { 22 | attr := rinq.Attr{Key: "foo", Value: ""} 23 | Expect(attr.String()).To(Equal("-foo")) 24 | }) 25 | 26 | It("uses 'bang' syntax for empty frozen attributes", func() { 27 | attr := rinq.Attr{Key: "foo", Value: "", IsFrozen: true} 28 | Expect(attr.String()).To(Equal("!foo")) 29 | }) 30 | 31 | It("escapes attributes that contain certain characters", func() { 32 | attr := rinq.Attr{Key: "foo key", Value: "bar value"} 33 | Expect(attr.String()).To(Equal(`"foo key"="bar value"`)) 34 | }) 35 | }) 36 | }) 37 | 38 | var _ = Describe("Set", func() { 39 | It("returns a non-frozen attribute", func() { 40 | attr := rinq.Set("foo", "bar") 41 | expected := rinq.Attr{Key: "foo", Value: "bar"} 42 | Expect(attr).To(Equal(expected)) 43 | }) 44 | }) 45 | 46 | var _ = Describe("Freeze", func() { 47 | It("returns a frozen attribute", func() { 48 | attr := rinq.Freeze("foo", "bar") 49 | expected := rinq.Attr{Key: "foo", Value: "bar", IsFrozen: true} 50 | Expect(attr).To(Equal(expected)) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/rinq/command_example_test.go: -------------------------------------------------------------------------------- 1 | // +build !without_amqp,!without_examples 2 | 3 | package rinq_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | . "github.com/rinq/rinq-go/src/rinq" 10 | "github.com/rinq/rinq-go/src/rinqamqp" 11 | ) 12 | 13 | // This example illustrates how to respond to a command request with an 14 | // application-defined failure. 15 | func ExampleResponse_fail() { 16 | peer, err := rinqamqp.DialEnv() 17 | if err != nil { 18 | panic(err) 19 | } 20 | defer peer.Stop() 21 | 22 | peer.Listen("my-api", func( 23 | ctx context.Context, 24 | req Request, 25 | res Response, 26 | ) { 27 | defer req.Payload.Close() 28 | 29 | res.Fail( 30 | "my-api-error", 31 | "the call to %s failed spectacularly!", 32 | req.Command, 33 | ) 34 | }) 35 | 36 | sess := peer.Session() 37 | defer sess.Destroy() 38 | 39 | in, err := sess.Call(context.Background(), "my-api", "test", nil) 40 | defer in.Close() 41 | 42 | fmt.Println(err) 43 | // Output: my-api-error: the call to test failed spectacularly! 44 | } 45 | -------------------------------------------------------------------------------- /src/rinq/command_test.go: -------------------------------------------------------------------------------- 1 | package rinq_test 2 | 3 | import ( 4 | "errors" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | "github.com/rinq/rinq-go/src/rinq" 9 | ) 10 | 11 | var _ = Describe("Failure", func() { 12 | Describe("Error", func() { 13 | It("includes both the type and the message", func() { 14 | err := rinq.Failure{Type: "", Message: ""} 15 | Expect(err.Error()).To(Equal(": ")) 16 | }) 17 | }) 18 | }) 19 | 20 | var _ = Describe("IsFailure", func() { 21 | It("returns true for failures", func() { 22 | r := rinq.IsFailure(rinq.Failure{}) 23 | Expect(r).To(BeTrue()) 24 | }) 25 | 26 | It("returns false for other error types", func() { 27 | r := rinq.IsFailure(errors.New("")) 28 | Expect(r).To(BeFalse()) 29 | }) 30 | }) 31 | 32 | var _ = Describe("IsFailureType", func() { 33 | It("returns true for failures with the same type", func() { 34 | r := rinq.IsFailureType("foo", rinq.Failure{Type: "foo"}) 35 | Expect(r).To(BeTrue()) 36 | }) 37 | 38 | It("returns false for failures with a different type", func() { 39 | r := rinq.IsFailureType("foo", rinq.Failure{Type: "bar"}) 40 | Expect(r).To(BeFalse()) 41 | }) 42 | 43 | It("returns false for other error types", func() { 44 | r := rinq.IsFailureType("foo", errors.New("")) 45 | Expect(r).To(BeFalse()) 46 | }) 47 | 48 | It("panics if the type is empty", func() { 49 | f := func() { 50 | rinq.IsFailureType("", errors.New("")) 51 | } 52 | Expect(f).Should(Panic()) 53 | }) 54 | }) 55 | 56 | var _ = Describe("FailureType", func() { 57 | It("returns the failure type", func() { 58 | r := rinq.FailureType(rinq.Failure{Type: "foo"}) 59 | Expect(r).To(Equal("foo")) 60 | }) 61 | 62 | It("returns empty string for other error types", func() { 63 | r := rinq.FailureType(errors.New("")) 64 | Expect(r).To(Equal("")) 65 | }) 66 | 67 | It("panics if the type is empty", func() { 68 | f := func() { 69 | rinq.FailureType(rinq.Failure{}) 70 | } 71 | Expect(f).Should(Panic()) 72 | }) 73 | }) 74 | 75 | var _ = Describe("IsCommandError", func() { 76 | It("returns true for Failure", func() { 77 | r := rinq.IsCommandError(rinq.Failure{Type: "foo"}) 78 | Expect(r).To(BeTrue()) 79 | }) 80 | 81 | It("returns true for CommandError", func() { 82 | r := rinq.IsCommandError(rinq.CommandError("")) 83 | Expect(r).To(BeTrue()) 84 | }) 85 | 86 | It("returns false for other error types", func() { 87 | r := rinq.IsCommandError(errors.New("")) 88 | Expect(r).To(BeFalse()) 89 | }) 90 | }) 91 | 92 | var _ = Describe("CommandError", func() { 93 | Describe("Error", func() { 94 | It("returns the message", func() { 95 | err := rinq.CommandError("") 96 | Expect(err.Error()).To(Equal("")) 97 | }) 98 | 99 | It("returns a message for the default value", func() { 100 | err := rinq.CommandError("") 101 | Expect(err.Error()).To(Equal("unexpected command error")) 102 | }) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /src/rinq/constraint/constraint.go: -------------------------------------------------------------------------------- 1 | package constraint 2 | 3 | import "github.com/rinq/rinq-go/src/internal/x/bufferpool" 4 | 5 | // Constraint is a boolean expression evaluated against session attribute values 6 | // to determine which sessions receive a multicast notification. 7 | // 8 | // See Session.NotifyMany() to send a multicast notification. 9 | type Constraint struct { 10 | Op op `json:"o,omitempty"` 11 | Terms []Constraint `json:"t,omitempty"` 12 | Key string `json:"k,omitempty"` 13 | Value string `json:"v,omitempty"` 14 | } 15 | 16 | // And returns a Constraint that evaluates to true if both c and con evaluate to 17 | // true. 18 | func (c Constraint) And(con Constraint) Constraint { 19 | return And(c, con) 20 | } 21 | 22 | // Or returns a Constraint that evaluates to true if at least one of c and con 23 | // evaluate to true. 24 | func (c Constraint) Or(con Constraint) Constraint { 25 | return Or(c, con) 26 | } 27 | 28 | // Validate returns nil if c is a valid constraint. 29 | func (c Constraint) Validate() error { 30 | v := &validator{} 31 | _, err := c.Accept(v) 32 | 33 | return err 34 | } 35 | 36 | // Accept calls the method on v that corresponds to the operation type of c. 37 | func (c Constraint) Accept(v Visitor, args ...interface{}) (interface{}, error) { 38 | switch c.Op { 39 | case noneOp: 40 | return v.None(args...) 41 | case withinOp: 42 | return v.Within(c.Value, c.Terms, args...) 43 | case equalOp: 44 | return v.Equal(c.Key, c.Value, args...) 45 | case notEqualOp: 46 | return v.NotEqual(c.Key, c.Value, args...) 47 | case notOp: 48 | return v.Not(c.Terms[0], args...) 49 | case andOp: 50 | return v.And(c.Terms, args...) 51 | case orOp: 52 | return v.Or(c.Terms, args...) 53 | default: 54 | panic("unrecognized constraint operation: " + c.Op) 55 | } 56 | } 57 | 58 | func (c Constraint) String() string { 59 | buf := bufferpool.Get() 60 | defer bufferpool.Put(buf) 61 | 62 | v := &stringer{buf, nil} 63 | _, _ = c.Accept(v) 64 | 65 | return buf.String() 66 | } 67 | 68 | // None is a Constraint that always evaluates to true, and hence provides 69 | // "no constraint" on the sessions that receive the notification. 70 | var None = Constraint{Op: noneOp} 71 | 72 | // Within returns a Constraint that evaluates to true when each constraint in 73 | // cons evaluates to true within the ns namespace. 74 | func Within(ns string, cons ...Constraint) Constraint { 75 | return Constraint{ 76 | Op: withinOp, 77 | Terms: cons, 78 | Value: ns, 79 | } 80 | } 81 | 82 | // Equal returns a Constraint that evaluates to true when the attribute k is 83 | // equal to v. 84 | func Equal(k, v string) Constraint { 85 | return Constraint{ 86 | Op: equalOp, 87 | Key: k, 88 | Value: v, 89 | } 90 | } 91 | 92 | // NotEqual returns a Constraint that evaluates to true when the attribute k is 93 | // not equal to v. 94 | func NotEqual(k, v string) Constraint { 95 | return Constraint{ 96 | Op: notEqualOp, 97 | Key: k, 98 | Value: v, 99 | } 100 | } 101 | 102 | // Empty returns a Constraint that evaluates to true when the attribute k has a 103 | // value equal to the empty string. 104 | func Empty(k string) Constraint { 105 | return Constraint{ 106 | Op: equalOp, 107 | Key: k, 108 | } 109 | } 110 | 111 | // NotEmpty returns a Constraint that evaluates to true when the attribute k has 112 | // a value not equal to the empty string. 113 | func NotEmpty(k string) Constraint { 114 | return Constraint{ 115 | Op: notEqualOp, 116 | Key: k, 117 | } 118 | } 119 | 120 | // Not returns a Constraint that evaluates to true when e evaluates to false, 121 | // and vice-versa. 122 | func Not(con Constraint) Constraint { 123 | return Constraint{ 124 | Op: notOp, 125 | Terms: []Constraint{con}, 126 | } 127 | } 128 | 129 | // And returns a Constraint that evaluates to true when all constraints in cons 130 | // evaluate to true. 131 | func And(cons ...Constraint) Constraint { 132 | return Constraint{ 133 | Op: andOp, 134 | Terms: cons, 135 | } 136 | } 137 | 138 | // Or returns a Constraint that evaluates to true when one or more of the 139 | // constraints in cons evaluate to true. 140 | func Or(cons ...Constraint) Constraint { 141 | return Constraint{ 142 | Op: orOp, 143 | Terms: cons, 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/rinq/constraint/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package constraint_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "constraint") 13 | } 14 | -------------------------------------------------------------------------------- /src/rinq/constraint/pkg.go: -------------------------------------------------------------------------------- 1 | // Package constraint provides functions for building session constraints for 2 | // use with rinq.Session.NotifyMany(). 3 | package constraint 4 | -------------------------------------------------------------------------------- /src/rinq/constraint/stringer.go: -------------------------------------------------------------------------------- 1 | package constraint 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/rinq/rinq-go/src/internal/x/repr" 7 | ) 8 | 9 | type stringer struct { 10 | buf *bytes.Buffer 11 | hasBraces []bool 12 | } 13 | 14 | func (s *stringer) None(...interface{}) (interface{}, error) { 15 | s.open() 16 | s.buf.WriteString("*") 17 | s.close() 18 | 19 | return nil, nil 20 | } 21 | 22 | func (s *stringer) Within(ns string, cons []Constraint, _ ...interface{}) (interface{}, error) { 23 | s.buf.WriteString(ns) 24 | s.buf.WriteString("::") 25 | s.join(", ", cons) 26 | 27 | return nil, nil 28 | } 29 | 30 | func (s *stringer) Equal(k, v string, _ ...interface{}) (interface{}, error) { 31 | s.open() 32 | 33 | if v == "" { 34 | s.buf.WriteRune('!') 35 | s.buf.WriteString(repr.Escape(k)) 36 | } else { 37 | s.buf.WriteString(repr.Escape(k)) 38 | s.buf.WriteRune('=') 39 | s.buf.WriteString(repr.Escape(v)) 40 | } 41 | 42 | s.close() 43 | 44 | return nil, nil 45 | } 46 | 47 | func (s *stringer) NotEqual(k, v string, _ ...interface{}) (interface{}, error) { 48 | s.open() 49 | 50 | if v == "" { 51 | s.buf.WriteString(repr.Escape(k)) 52 | } else { 53 | s.buf.WriteString(repr.Escape(k)) 54 | s.buf.WriteString("!=") 55 | s.buf.WriteString(repr.Escape(v)) 56 | } 57 | 58 | s.close() 59 | 60 | return nil, nil 61 | } 62 | 63 | func (s *stringer) Not(con Constraint, _ ...interface{}) (interface{}, error) { 64 | s.open() 65 | s.buf.WriteString("! ") 66 | _, _ = con.Accept(s) 67 | s.close() 68 | 69 | return nil, nil 70 | } 71 | 72 | func (s *stringer) And(cons []Constraint, _ ...interface{}) (interface{}, error) { 73 | s.join(", ", cons) 74 | 75 | return nil, nil 76 | } 77 | 78 | func (s *stringer) Or(cons []Constraint, _ ...interface{}) (interface{}, error) { 79 | s.join("|", cons) 80 | 81 | return nil, nil 82 | } 83 | 84 | func (s *stringer) join(sep string, cons []Constraint) { 85 | if len(cons) == 1 { 86 | _, _ = cons[0].Accept(s) 87 | return 88 | } 89 | 90 | s.buf.WriteRune('{') 91 | s.push(true) 92 | 93 | for i, con := range cons { 94 | if i != 0 { 95 | s.buf.WriteString(sep) 96 | } 97 | _, _ = con.Accept(s) 98 | } 99 | 100 | s.pop() 101 | s.buf.WriteRune('}') 102 | } 103 | 104 | func (s *stringer) push(b bool) { 105 | s.hasBraces = append(s.hasBraces, b) 106 | } 107 | 108 | func (s *stringer) pop() { 109 | s.hasBraces = s.hasBraces[:len(s.hasBraces)-1] 110 | } 111 | 112 | func (s *stringer) needsBraces() bool { 113 | if len(s.hasBraces) == 0 { 114 | return true 115 | } 116 | 117 | return !s.hasBraces[len(s.hasBraces)-1] 118 | } 119 | 120 | func (s *stringer) open() { 121 | if s.needsBraces() { 122 | s.buf.WriteRune('{') 123 | } 124 | 125 | s.push(true) 126 | } 127 | 128 | func (s *stringer) close() { 129 | s.pop() 130 | 131 | if s.needsBraces() { 132 | s.buf.WriteRune('}') 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/rinq/constraint/validator.go: -------------------------------------------------------------------------------- 1 | package constraint 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/rinq/rinq-go/src/internal/namespaces" 7 | ) 8 | 9 | type validator struct{} 10 | 11 | func (v *validator) None(...interface{}) (interface{}, error) { 12 | return nil, nil 13 | } 14 | 15 | func (v *validator) Within(ns string, cons []Constraint, _ ...interface{}) (interface{}, error) { 16 | if err := namespaces.Validate(ns); err != nil { 17 | return nil, errors.New("WITHIN constraint has invalid namespace: " + err.Error()) 18 | } 19 | 20 | if len(cons) == 0 { 21 | return nil, errors.New("WITHIN constraint has no terms") 22 | } 23 | 24 | for _, con := range cons { 25 | if _, err := con.Accept(v); err != nil { 26 | return nil, err 27 | } 28 | } 29 | 30 | return nil, nil 31 | } 32 | 33 | func (v *validator) Equal(string, string, ...interface{}) (interface{}, error) { 34 | return nil, nil 35 | } 36 | 37 | func (v *validator) NotEqual(string, string, ...interface{}) (interface{}, error) { 38 | return nil, nil 39 | } 40 | 41 | func (v *validator) Not(con Constraint, _ ...interface{}) (interface{}, error) { 42 | return con.Accept(v) 43 | } 44 | 45 | func (v *validator) And(cons []Constraint, _ ...interface{}) (interface{}, error) { 46 | if len(cons) == 0 { 47 | return nil, errors.New("AND constraint has no terms") 48 | } 49 | 50 | for _, con := range cons { 51 | if _, err := con.Accept(v); err != nil { 52 | return nil, err 53 | } 54 | } 55 | 56 | return nil, nil 57 | } 58 | 59 | func (v *validator) Or(cons []Constraint, _ ...interface{}) (interface{}, error) { 60 | if len(cons) == 0 { 61 | return nil, errors.New("OR constraint has no terms") 62 | } 63 | 64 | for _, con := range cons { 65 | if _, err := con.Accept(v); err != nil { 66 | return nil, err 67 | } 68 | } 69 | 70 | return nil, nil 71 | } 72 | -------------------------------------------------------------------------------- /src/rinq/constraint/visitor.go: -------------------------------------------------------------------------------- 1 | package constraint 2 | 3 | type op string 4 | 5 | const ( 6 | noneOp op = "*" 7 | withinOp op = "ns" 8 | equalOp op = "=" 9 | notEqualOp op = "!=" 10 | notOp op = "!" 11 | andOp op = "&" 12 | orOp op = "|" 13 | ) 14 | 15 | // Visitor is used to walk a constraint hierarchy. 16 | type Visitor interface { 17 | None(args ...interface{}) (interface{}, error) 18 | Within(ns string, cons []Constraint, args ...interface{}) (interface{}, error) 19 | Equal(k, v string, args ...interface{}) (interface{}, error) 20 | NotEqual(k, v string, args ...interface{}) (interface{}, error) 21 | Not(con Constraint, args ...interface{}) (interface{}, error) 22 | And(cons []Constraint, args ...interface{}) (interface{}, error) 23 | Or(cons []Constraint, args ...interface{}) (interface{}, error) 24 | } 25 | -------------------------------------------------------------------------------- /src/rinq/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package rinq_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "rinq") 13 | } 14 | -------------------------------------------------------------------------------- /src/rinq/ident/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package ident_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "ident") 13 | } 14 | -------------------------------------------------------------------------------- /src/rinq/ident/message_id.go: -------------------------------------------------------------------------------- 1 | package ident 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | // MessageID uniquely identifies a message that originated from a session. 10 | type MessageID struct { 11 | Ref Ref 12 | Seq uint32 13 | } 14 | 15 | // ParseMessageID parses a string representation of a message ID. 16 | func ParseMessageID(str string) (id MessageID, err error) { 17 | matches := messageIDPattern.FindStringSubmatch(str) 18 | 19 | if len(matches) != 0 { 20 | // Read the peer ID clock component ... 21 | var value uint64 22 | value, err = strconv.ParseUint(matches[1], 16, 64) 23 | if err != nil { 24 | return 25 | } 26 | id.Ref.ID.Peer.Clock = value 27 | 28 | // Read the peer ID random component ... 29 | value, err = strconv.ParseUint(matches[2], 16, 16) 30 | if err != nil { 31 | return 32 | } 33 | id.Ref.ID.Peer.Rand = uint16(value) 34 | 35 | // Read the session ID sequence component ... 36 | value, err = strconv.ParseUint(matches[3], 10, 32) 37 | if err != nil { 38 | return 39 | } 40 | id.Ref.ID.Seq = uint32(value) 41 | 42 | // Read the session version ... 43 | value, err = strconv.ParseUint(matches[4], 10, 32) 44 | if err != nil { 45 | return 46 | } 47 | id.Ref.Rev = Revision(value) 48 | 49 | // Read the message ID sequence component ... 50 | value, err = strconv.ParseUint(matches[5], 10, 32) 51 | if err != nil { 52 | return 53 | } 54 | id.Seq = uint32(value) 55 | } 56 | 57 | err = id.Validate() 58 | return 59 | } 60 | 61 | // Validate returns nil if the ID is valid. 62 | func (id MessageID) Validate() error { 63 | if id.Ref.Validate() == nil && id.Seq != 0 { 64 | return nil 65 | } 66 | 67 | return fmt.Errorf("message ID %s is invalid", id) 68 | } 69 | 70 | // ShortString returns a string representation based on the session's short 71 | // string representation. 72 | func (id MessageID) ShortString() string { 73 | return fmt.Sprintf("%s#%d", id.Ref.ShortString(), id.Seq) 74 | } 75 | 76 | func (id MessageID) String() string { 77 | return fmt.Sprintf("%s#%d", id.Ref, id.Seq) 78 | } 79 | 80 | var messageIDPattern *regexp.Regexp 81 | 82 | func init() { 83 | messageIDPattern = regexp.MustCompile( 84 | `^(.+)\-(.+)\.(.+)@(.+)#(.+)$`, 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /src/rinq/ident/message_id_test.go: -------------------------------------------------------------------------------- 1 | package ident_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/ginkgo/extensions/table" 6 | . "github.com/onsi/gomega" 7 | . "github.com/rinq/rinq-go/src/rinq/ident" 8 | ) 9 | 10 | var _ = Describe("MessageID", func() { 11 | var sessionRef = Ref{ 12 | ID: SessionID{ 13 | Peer: PeerID{ 14 | Clock: 0x0123456789abcdef, 15 | Rand: 0x0bad, 16 | }, 17 | Seq: 123, 18 | }, 19 | Rev: 456, 20 | } 21 | 22 | Describe("ParseMessageID", func() { 23 | It("parses a human readable ID", func() { 24 | id, err := ParseMessageID("123456789ABCDEF-0BAD.123@456#789") 25 | 26 | Expect(err).ShouldNot(HaveOccurred()) 27 | Expect(id.String()).To(Equal("123456789ABCDEF-0BAD.123@456#789")) 28 | }) 29 | 30 | DescribeTable( 31 | "returns an error if the string is malformed", 32 | func(id string) { 33 | _, err := ParseMessageID(id) 34 | 35 | Expect(err).Should(HaveOccurred()) 36 | }, 37 | Entry("malformed", ""), 38 | Entry("zero peer clock component", "0-1.1@0#1"), 39 | Entry("zero peer random component", "1-0.1@0#1"), 40 | Entry("zero message seq", "1-1.1@0#0"), 41 | Entry("invalid peer clock component", "x-1.1@0#1"), 42 | Entry("invalid peer random component", "1-x.1@0#1"), 43 | Entry("invalid session sequence", "1-1.x@0#1"), 44 | Entry("invalid session revision", "1-1.1@x#1"), 45 | Entry("invalid message sequence", "1-1.1@0#x"), 46 | ) 47 | }) 48 | 49 | DescribeTable( 50 | "Validate", 51 | func(subject MessageID, isValid bool) { 52 | if isValid { 53 | Expect(subject.Validate()).To(Succeed()) 54 | } else { 55 | Expect(subject.Validate()).Should(HaveOccurred()) 56 | } 57 | }, 58 | Entry("zero struct", MessageID{}, false), 59 | Entry("zero session", MessageID{Seq: 1}, false), 60 | Entry("zero seq", MessageID{Ref: sessionRef}, false), 61 | Entry("non-zero struct", MessageID{Ref: sessionRef, Seq: 1}, true), 62 | ) 63 | 64 | Describe("ShortString", func() { 65 | It("returns a human readable ID", func() { 66 | subject := MessageID{Ref: sessionRef, Seq: 789} 67 | Expect(subject.ShortString()).To(Equal("0BAD.123@456#789")) 68 | }) 69 | }) 70 | 71 | Describe("String", func() { 72 | It("returns a human readable ID", func() { 73 | subject := MessageID{Ref: sessionRef, Seq: 789} 74 | Expect(subject.String()).To(Equal("123456789ABCDEF-0BAD.123@456#789")) 75 | }) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /src/rinq/ident/peer_id.go: -------------------------------------------------------------------------------- 1 | package ident 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | // PeerID uniquely identifies a peer within a network. 11 | // 12 | // Peer IDs contain a clock component and a random 16-bit integer component. 13 | // They are rendered in hexadecimal encoding, with a hypen separating the two 14 | // components, such as "58AEE146-191C". 15 | // 16 | // For a given network, the random component is guaranteed to be unique at any 17 | // given time; and, assuming a stable system clock it is highly likely that the 18 | // ID is unique across time. This makes peer IDs useful for tracking peer 19 | // behavior in logs. 20 | // 21 | // All other IDs generated by a peer, such as SessionID and MessageID are 22 | // derived from the peer ID. 23 | type PeerID struct { 24 | // Clock is a time-based portion of the ID, this helps uniquely identify 25 | // peer IDs over longer time-scales, such as when looking back through 26 | // logs, etc. 27 | Clock uint64 28 | 29 | // Rand is a unique number identifying this peer within a network at any 30 | // given time. It is generated randomly and then reserved when the peer 31 | // connects to the network. 32 | Rand uint16 33 | } 34 | 35 | // NewPeerID creates a new ID struct. There is no guarantee that the ID is 36 | // unique until the peer is connected to a network. 37 | func NewPeerID() PeerID { 38 | return PeerID{ 39 | uint64(time.Now().Unix()), 40 | uint16(rand.Intn(math.MaxUint16-1)) + 1, 41 | } 42 | } 43 | 44 | // Validate returns an error if the peer ID is not valid. 45 | // 46 | // Neither the Clock nor Rand component may be zero. 47 | func (id PeerID) Validate() error { 48 | if id.Clock != 0 && id.Rand != 0 { 49 | return nil 50 | } 51 | 52 | return fmt.Errorf("peer ID %s is invalid", id) 53 | } 54 | 55 | // Session returns a new session ID for this peer with seq as the sequence 56 | // number. 57 | func (id PeerID) Session(seq uint32) SessionID { 58 | return SessionID{ 59 | Peer: id, 60 | Seq: seq, 61 | } 62 | } 63 | 64 | // ShortString returns a string representation of the peer ID without the clock 65 | // component (e.g. "191C"). 66 | func (id PeerID) ShortString() string { 67 | return fmt.Sprintf( 68 | "%04X", 69 | id.Rand, 70 | ) 71 | } 72 | 73 | // String returns a string representation including both the Clock and Rand 74 | // components (e.g. "58AEE146-191C"). 75 | func (id PeerID) String() string { 76 | return fmt.Sprintf( 77 | "%X-%04X", 78 | id.Clock, 79 | id.Rand, 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /src/rinq/ident/peer_id_test.go: -------------------------------------------------------------------------------- 1 | package ident_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/ginkgo/extensions/table" 6 | . "github.com/onsi/gomega" 7 | . "github.com/rinq/rinq-go/src/rinq/ident" 8 | ) 9 | 10 | var _ = Describe("PeerID", func() { 11 | Describe("NewPeerID", func() { 12 | It("returns a valid ID", func() { 13 | subject := NewPeerID() 14 | err := subject.Validate() 15 | Expect(err).ShouldNot(HaveOccurred()) 16 | }) 17 | }) 18 | 19 | DescribeTable( 20 | "Validate", 21 | func(subject PeerID, isValid bool) { 22 | if isValid { 23 | Expect(subject.Validate()).To(Succeed()) 24 | } else { 25 | Expect(subject.Validate()).Should(HaveOccurred()) 26 | } 27 | }, 28 | Entry("zero struct", PeerID{}, false), 29 | Entry("zero clock component", PeerID{Rand: 1}, false), 30 | Entry("zero random component", PeerID{Clock: 1}, false), 31 | Entry("non-zero struct", PeerID{Clock: 1, Rand: 1}, true), 32 | ) 33 | 34 | Describe("Session", func() { 35 | It("returns a new SessionID with the given sequence number", func() { 36 | subject := NewPeerID() 37 | sessionID := subject.Session(123) 38 | Expect(sessionID.Peer).To(Equal(subject)) 39 | Expect(sessionID.Seq).To(Equal(uint32(123))) 40 | }) 41 | }) 42 | 43 | Describe("ShortString", func() { 44 | It("returns a human readable ID", func() { 45 | subject := PeerID{Clock: 0x0123456789abcdef, Rand: 0x0bad} 46 | Expect(subject.ShortString()).To(Equal("0BAD")) 47 | }) 48 | }) 49 | 50 | Describe("String", func() { 51 | It("returns a human readable ID", func() { 52 | subject := PeerID{Clock: 0x0123456789abcdef, Rand: 0x0bad} 53 | Expect(subject.String()).To(Equal("123456789ABCDEF-0BAD")) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/rinq/ident/pkg.go: -------------------------------------------------------------------------------- 1 | // Package ident contains types that represent various Rinq identifiers. 2 | package ident 3 | -------------------------------------------------------------------------------- /src/rinq/ident/ref.go: -------------------------------------------------------------------------------- 1 | package ident 2 | 3 | import "fmt" 4 | 5 | // Revision holds the "version" of a session. A session's revision is 6 | // incremented when a change is made to its attribute table. A session that has 7 | // never been modified, and hence has no attributes always has a revision of 0. 8 | type Revision uint32 9 | 10 | // Ref refers to a session at a specific revision. 11 | type Ref struct { 12 | ID SessionID 13 | Rev Revision 14 | } 15 | 16 | // Validate returns nil if the Ref is valid. 17 | func (ref Ref) Validate() error { 18 | if ref.ID.Validate() == nil { 19 | return nil 20 | } 21 | 22 | return fmt.Errorf("session reference %s is invalid", ref) 23 | } 24 | 25 | // Before returns true if this ref's revision is before r. 26 | func (ref Ref) Before(r Ref) bool { 27 | if ref.ID != r.ID { 28 | panic("can not compare references from different sessions") 29 | } 30 | 31 | return ref.Rev < r.Rev 32 | } 33 | 34 | // After returns true if this ref's revision is after r. 35 | func (ref Ref) After(r Ref) bool { 36 | if ref.ID != r.ID { 37 | panic("can not compare references from different sessions") 38 | } 39 | 40 | return ref.Rev > r.Rev 41 | } 42 | 43 | // Message returns a new message ID derived from this ref with seq as the 44 | // sequence number. 45 | func (ref Ref) Message(seq uint32) MessageID { 46 | return MessageID{ 47 | Ref: ref, 48 | Seq: seq, 49 | } 50 | } 51 | 52 | // ShortString returns a string representation based on the session's short 53 | // string representation. 54 | func (ref Ref) ShortString() string { 55 | return fmt.Sprintf("%s@%d", ref.ID.ShortString(), ref.Rev) 56 | } 57 | 58 | func (ref Ref) String() string { 59 | return fmt.Sprintf("%s@%d", ref.ID, ref.Rev) 60 | } 61 | -------------------------------------------------------------------------------- /src/rinq/ident/ref_test.go: -------------------------------------------------------------------------------- 1 | package ident_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/ginkgo/extensions/table" 6 | . "github.com/onsi/gomega" 7 | . "github.com/rinq/rinq-go/src/rinq/ident" 8 | ) 9 | 10 | var _ = Describe("Ref", func() { 11 | var sessionID = SessionID{ 12 | Peer: PeerID{ 13 | Clock: 0x0123456789abcdef, 14 | Rand: 0x0bad, 15 | }, 16 | Seq: 123, 17 | } 18 | 19 | DescribeTable( 20 | "Validate", 21 | func(subject Ref, isValid bool) { 22 | if isValid { 23 | Expect(subject.Validate()).To(Succeed()) 24 | } else { 25 | Expect(subject.Validate()).Should(HaveOccurred()) 26 | } 27 | }, 28 | Entry("zero struct", Ref{}, false), 29 | Entry("non-zero struct", Ref{ID: sessionID}, true), 30 | ) 31 | 32 | Describe("Before", func() { 33 | DescribeTable( 34 | "compares the revision number", 35 | func(a, b Ref, expected bool) { 36 | Expect(a.Before(b)).To(Equal(expected)) 37 | }, 38 | Entry("a == b", SessionID{}.At(1), SessionID{}.At(1), false), 39 | Entry("a < b", SessionID{}.At(1), SessionID{}.At(2), true), 40 | Entry("a > b", SessionID{}.At(2), SessionID{}.At(1), false), 41 | ) 42 | 43 | It("panics if the session IDs are not the same", func() { 44 | Expect(func() { 45 | a := SessionID{Seq: 1}.At(0) 46 | b := SessionID{Seq: 2}.At(0) 47 | a.Before(b) 48 | }).Should(Panic()) 49 | }) 50 | }) 51 | 52 | Describe("After", func() { 53 | DescribeTable( 54 | "compares the revision number", 55 | func(a, b Ref, expected bool) { 56 | Expect(a.After(b)).To(Equal(expected)) 57 | }, 58 | Entry("a == b", SessionID{}.At(1), SessionID{}.At(1), false), 59 | Entry("a < b", SessionID{}.At(1), SessionID{}.At(2), false), 60 | Entry("a > b", SessionID{}.At(2), SessionID{}.At(1), true), 61 | ) 62 | 63 | It("panics if the session IDs are not the same", func() { 64 | Expect(func() { 65 | a := SessionID{Seq: 1}.At(0) 66 | b := SessionID{Seq: 2}.At(0) 67 | a.After(b) 68 | }).Should(Panic()) 69 | }) 70 | }) 71 | 72 | Describe("Message", func() { 73 | It("returns a new MessageID with the given sequence number", func() { 74 | subject := Ref{ID: sessionID, Rev: 456} 75 | messageID := subject.Message(123) 76 | Expect(messageID.Ref).To(Equal(subject)) 77 | Expect(messageID.Seq).To(Equal(uint32(123))) 78 | }) 79 | }) 80 | 81 | Describe("String", func() { 82 | It("returns a human readable string", func() { 83 | subject := Ref{ID: sessionID, Rev: 456} 84 | Expect(subject.String()).To(Equal("123456789ABCDEF-0BAD.123@456")) 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /src/rinq/ident/session_id.go: -------------------------------------------------------------------------------- 1 | package ident 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | // SessionID uniquely identifies a session within a network. 10 | // 11 | // Session IDs contain a peer component, and a 32-bit sequence component. 12 | // They are rendereds as a peer ID, followed by a period, then the sequence 13 | // component as a decimal, such as "58AEE146-191C.45". 14 | // 15 | // Because the peer ID is embedded, the same uniqueness guarantees apply to the 16 | // session ID as to the peer ID. 17 | type SessionID struct { 18 | // Peer is the ID of the peer that owns the session. 19 | Peer PeerID 20 | 21 | // Seq is a monotonically increasing sequence allocated to each session in 22 | // the order it is created by the owning peer. Application sessions begin 23 | // with a sequence value of 1. The sequnce value zero is reserved for the 24 | // "zero-session", which is used for internal operations. 25 | Seq uint32 26 | } 27 | 28 | // ParseSessionID parses a string representation of a session ID. 29 | func ParseSessionID(str string) (id SessionID, err error) { 30 | matches := sessionIDPattern.FindStringSubmatch(str) 31 | 32 | if len(matches) != 0 { 33 | // Read the peer ID clock component ... 34 | var value uint64 35 | value, err = strconv.ParseUint(matches[1], 16, 64) 36 | if err != nil { 37 | return 38 | } 39 | id.Peer.Clock = value 40 | 41 | // Read the peer ID random component ... 42 | value, err = strconv.ParseUint(matches[2], 16, 16) 43 | if err != nil { 44 | return 45 | } 46 | id.Peer.Rand = uint16(value) 47 | 48 | // Read the session ID sequence component ... 49 | value, err = strconv.ParseUint(matches[3], 10, 32) 50 | if err != nil { 51 | return 52 | } 53 | id.Seq = uint32(value) 54 | } 55 | 56 | err = id.Validate() 57 | return 58 | } 59 | 60 | // Validate returns an error if the session ID is not valid. 61 | // 62 | // The session ID is valid if the embedded peer ID is valid. 63 | func (id SessionID) Validate() error { 64 | if id.Peer.Validate() == nil { 65 | return nil 66 | } 67 | 68 | return fmt.Errorf("session ID %s is invalid", id) 69 | } 70 | 71 | // At returns a Ref for this session ID. 72 | func (id SessionID) At(rev Revision) Ref { 73 | return Ref{ID: id, Rev: rev} 74 | } 75 | 76 | // ShortString returns a string representation of the session ID based on the 77 | // peer IDs short representation (e.g. "191C.45"). 78 | func (id SessionID) ShortString() string { 79 | return fmt.Sprintf("%s.%d", id.Peer.ShortString(), id.Seq) 80 | } 81 | 82 | // String returns a string representation of the session ID based on the full 83 | // peer ID (e.g. "58AEE146-191C.45"). 84 | func (id SessionID) String() string { 85 | return fmt.Sprintf("%s.%d", id.Peer, id.Seq) 86 | } 87 | 88 | var sessionIDPattern *regexp.Regexp 89 | 90 | func init() { 91 | sessionIDPattern = regexp.MustCompile( 92 | `^(.+)\-(.+)\.(.+)`, 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /src/rinq/ident/session_id_test.go: -------------------------------------------------------------------------------- 1 | package ident_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/ginkgo/extensions/table" 6 | . "github.com/onsi/gomega" 7 | . "github.com/rinq/rinq-go/src/rinq/ident" 8 | ) 9 | 10 | var _ = Describe("SessionID", func() { 11 | peerID := PeerID{ 12 | Clock: 0x0123456789abcdef, 13 | Rand: 0x0bad, 14 | } 15 | 16 | Describe("ParseSessionID", func() { 17 | It("parses a human readable ID", func() { 18 | id, err := ParseSessionID("123456789ABCDEF-0BAD.123") 19 | 20 | Expect(err).ShouldNot(HaveOccurred()) 21 | Expect(id.String()).To(Equal("123456789ABCDEF-0BAD.123")) 22 | }) 23 | 24 | DescribeTable( 25 | "returns an error if the string is malformed", 26 | func(id string) { 27 | _, err := ParseSessionID(id) 28 | 29 | Expect(err).Should(HaveOccurred()) 30 | }, 31 | Entry("malformed", ""), 32 | Entry("zero peer clock component", "0-1"), 33 | Entry("zero peer random component", "1-0.1"), 34 | Entry("invalid peer clock component", "x-1.1"), 35 | Entry("invalid peer random component", "1-x.1"), 36 | Entry("invalid session sequence", "1-1.x"), 37 | ) 38 | }) 39 | 40 | DescribeTable( 41 | "Validate", 42 | func(subject SessionID, isValid bool) { 43 | if isValid { 44 | Expect(subject.Validate()).To(Succeed()) 45 | } else { 46 | Expect(subject.Validate()).Should(HaveOccurred()) 47 | } 48 | }, 49 | Entry("zero struct", SessionID{}, false), 50 | Entry("zero peer", SessionID{Seq: 1}, false), 51 | Entry("non-zero struct", SessionID{Peer: peerID, Seq: 1}, true), 52 | ) 53 | 54 | Describe("At", func() { 55 | It("creates a ref at the given version", func() { 56 | subject := SessionID{Peer: peerID, Seq: 123} 57 | ref := subject.At(456) 58 | Expect(ref).To(Equal(Ref{ID: subject, Rev: 456})) 59 | }) 60 | }) 61 | 62 | Describe("ShortString", func() { 63 | It("returns a human readable ID", func() { 64 | subject := SessionID{Peer: peerID, Seq: 123} 65 | Expect(subject.ShortString()).To(Equal("0BAD.123")) 66 | }) 67 | }) 68 | 69 | Describe("String", func() { 70 | It("returns a human readable ID", func() { 71 | subject := SessionID{Peer: peerID, Seq: 123} 72 | Expect(subject.String()).To(Equal("123456789ABCDEF-0BAD.123")) 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/rinq/ident/validate.go: -------------------------------------------------------------------------------- 1 | package ident 2 | 3 | type validatable interface { 4 | Validate() error 5 | } 6 | 7 | // MustValidate panics if v.Validate() returns an error. 8 | func MustValidate(v validatable) { 9 | if err := v.Validate(); err != nil { 10 | panic(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/rinq/ident/validate_test.go: -------------------------------------------------------------------------------- 1 | package ident_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | . "github.com/rinq/rinq-go/src/rinq/ident" 7 | ) 8 | 9 | var _ = Describe("MustValidate", func() { 10 | It("panics if the value is invalid", func() { 11 | Expect(func() { 12 | MustValidate(PeerID{}) 13 | }).To(Panic()) 14 | }) 15 | 16 | It("does not panic if the value is valid", func() { 17 | Expect(func() { 18 | MustValidate(NewPeerID()) 19 | }).NotTo(Panic()) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/rinq/notification.go: -------------------------------------------------------------------------------- 1 | package rinq 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rinq/rinq-go/src/rinq/constraint" 7 | "github.com/rinq/rinq-go/src/rinq/ident" 8 | ) 9 | 10 | // Notification holds information about an inter-session notification. 11 | type Notification struct { 12 | // ID uniquely identifies the notification. 13 | ID ident.MessageID 14 | 15 | // Source refers to the session that sent the notification. 16 | Source Revision 17 | 18 | // Namespace is the notification namespace. Namespaces are used to route 19 | // notifications to only those sessions that intend to handle them. 20 | Namespace string 21 | 22 | // Type is an application-defined notification type. 23 | Type string 24 | 25 | // Payload contains optional application-defined information. The handler 26 | // that accepts the notifiation is responsible for closing the payload, 27 | // however there is no requirement that the payload be closed during the 28 | // execution of the handler. 29 | Payload *Payload 30 | 31 | // IsMulticast is true if the notification was (potentially) sent to more 32 | // than one session. 33 | IsMulticast bool 34 | 35 | // For multicast notifications, Constraint contains the attributes used as 36 | // criteria for selecting which sessions receive the notification. The 37 | // constraint is nil if IsMulticast is false. 38 | Constraint constraint.Constraint 39 | } 40 | 41 | // NotificationHandler is a callback-function invoked when an inter-session 42 | // notification is received. 43 | // 44 | // Notifications can only be received for namespaces that a session is listening 45 | // to. See Session.Listen() to start listening. 46 | // 47 | // The handler is responsible for closing n.Payload, however there is no 48 | // requirement that the payload be closed during the execution of the handler. 49 | type NotificationHandler func( 50 | ctx context.Context, 51 | target Session, 52 | n Notification, 53 | ) 54 | -------------------------------------------------------------------------------- /src/rinq/options/env.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/jmalloc/twelf/src/twelf" 7 | "github.com/rinq/rinq-go/src/internal/x/env" 8 | ) 9 | 10 | // FromEnv returns peer options with values read from environment variables. 11 | // 12 | // The environment variables are listed below. 13 | // 14 | // - RINQ_DEFAULT_TIMEOUT (duration in milliseconds, non-zero) 15 | // - RINQ_LOG_DEBUG (boolean 'true' or 'false') 16 | // - RINQ_COMMAND_WORKERS (positive integer, non-zero) 17 | // - RINQ_SESSION_WORKERS (positive integer, non-zero) 18 | // - RINQ_PRUNE_INTERVAL (duration in milliseconds, non-zero) 19 | // - RINQ_PRODUCT (string) 20 | func FromEnv() ([]Option, error) { 21 | var o []Option 22 | 23 | t, ok, err := env.Duration("RINQ_DEFAULT_TIMEOUT") 24 | if err != nil { 25 | return nil, err 26 | } else if ok { 27 | o = append(o, DefaultTimeout(t)) 28 | } 29 | 30 | debug, ok, err := env.Bool("RINQ_LOG_DEBUG") 31 | if err != nil { 32 | return nil, err 33 | } else if ok { 34 | l := &twelf.StandardLogger{CaptureDebug: debug} 35 | o = append(o, Logger(l)) 36 | } 37 | 38 | n, ok, err := env.UInt("RINQ_COMMAND_WORKERS") 39 | if err != nil { 40 | return nil, err 41 | } else if ok { 42 | o = append(o, CommandWorkers(n)) 43 | } 44 | 45 | n, ok, err = env.UInt("RINQ_SESSION_WORKERS") 46 | if err != nil { 47 | return nil, err 48 | } else if ok { 49 | o = append(o, SessionWorkers(n)) 50 | } 51 | 52 | t, ok, err = env.Duration("RINQ_PRUNE_INTERVAL") 53 | if err != nil { 54 | return nil, err 55 | } else if ok { 56 | o = append(o, PruneInterval(t)) 57 | } 58 | 59 | if p := os.Getenv("RINQ_PRODUCT"); p != "" { 60 | o = append(o, Product(p)) 61 | } 62 | 63 | return o, nil 64 | } 65 | -------------------------------------------------------------------------------- /src/rinq/options/env_test.go: -------------------------------------------------------------------------------- 1 | package options_test 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/jmalloc/twelf/src/twelf" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | "github.com/rinq/rinq-go/src/rinq/options" 11 | ) 12 | 13 | var _ = Describe("FromEnv", func() { 14 | AfterEach(func() { 15 | os.Setenv("RINQ_DEFAULT_TIMEOUT", "") 16 | os.Setenv("RINQ_LOG_DEBUG", "") 17 | os.Setenv("RINQ_COMMAND_WORKERS", "") 18 | os.Setenv("RINQ_SESSION_WORKERS", "") 19 | os.Setenv("RINQ_PRUNE_INTERVAL", "") 20 | os.Setenv("RINQ_PRODUCT", "") 21 | }) 22 | 23 | It("returns an empty slice when no environment variables are set", func() { 24 | o, err := options.FromEnv() 25 | 26 | Expect(err).NotTo(HaveOccurred()) 27 | Expect(o).To(HaveLen(0)) 28 | }) 29 | 30 | Context("RINQ_DEFAULT_TIMEOUT", func() { 31 | It("returns a DefaultTimeout option", func() { 32 | os.Setenv("RINQ_DEFAULT_TIMEOUT", "500") 33 | o, err := options.FromEnv() 34 | 35 | Expect(err).NotTo(HaveOccurred()) 36 | 37 | opts, err := options.NewOptions(o...) 38 | 39 | Expect(err).NotTo(HaveOccurred()) 40 | Expect(opts.DefaultTimeout).To(Equal(500 * time.Millisecond)) 41 | }) 42 | 43 | It("returns an error if the value is not a positive integer", func() { 44 | os.Setenv("RINQ_DEFAULT_TIMEOUT", "-500") 45 | _, err := options.FromEnv() 46 | 47 | Expect(err).To(HaveOccurred()) 48 | }) 49 | }) 50 | 51 | Context("RINQ_LOG_DEBUG", func() { 52 | It("returns a Logger option when set to true", func() { 53 | os.Setenv("RINQ_LOG_DEBUG", "true") 54 | o, err := options.FromEnv() 55 | 56 | Expect(err).NotTo(HaveOccurred()) 57 | 58 | opts, err := options.NewOptions(o...) 59 | 60 | Expect(err).NotTo(HaveOccurred()) 61 | Expect(opts.Logger).To(Equal( 62 | &twelf.StandardLogger{CaptureDebug: true}, 63 | )) 64 | }) 65 | 66 | It("returns a Logger option when set to false", func() { 67 | os.Setenv("RINQ_LOG_DEBUG", "false") 68 | o, err := options.FromEnv() 69 | 70 | Expect(err).NotTo(HaveOccurred()) 71 | 72 | opts, err := options.NewOptions(o...) 73 | 74 | Expect(err).NotTo(HaveOccurred()) 75 | Expect(opts.Logger).To(Equal( 76 | &twelf.StandardLogger{CaptureDebug: false}, 77 | )) 78 | }) 79 | 80 | It("returns an error if the value is not a boolean", func() { 81 | os.Setenv("RINQ_LOG_DEBUG", "invalid") 82 | _, err := options.FromEnv() 83 | 84 | Expect(err).To(HaveOccurred()) 85 | }) 86 | }) 87 | 88 | Context("RINQ_COMMAND_WORKERS", func() { 89 | It("returns a CommandWorkers option", func() { 90 | os.Setenv("RINQ_COMMAND_WORKERS", "15") 91 | o, err := options.FromEnv() 92 | 93 | Expect(err).NotTo(HaveOccurred()) 94 | 95 | opts, err := options.NewOptions(o...) 96 | 97 | Expect(err).NotTo(HaveOccurred()) 98 | Expect(opts.CommandWorkers).To(Equal(uint(15))) 99 | }) 100 | 101 | It("returns an error if the value is not a positive integer", func() { 102 | os.Setenv("RINQ_COMMAND_WORKERS", "-500") 103 | _, err := options.FromEnv() 104 | 105 | Expect(err).To(HaveOccurred()) 106 | }) 107 | }) 108 | 109 | Context("RINQ_SESSION_WORKERS", func() { 110 | It("returns a SessionWorkers option", func() { 111 | os.Setenv("RINQ_SESSION_WORKERS", "25") 112 | o, err := options.FromEnv() 113 | 114 | Expect(err).NotTo(HaveOccurred()) 115 | 116 | opts, err := options.NewOptions(o...) 117 | 118 | Expect(err).NotTo(HaveOccurred()) 119 | Expect(opts.SessionWorkers).To(Equal(uint(25))) 120 | }) 121 | 122 | It("returns an error if the value is not a positive integer", func() { 123 | os.Setenv("RINQ_SESSION_WORKERS", "-500") 124 | _, err := options.FromEnv() 125 | 126 | Expect(err).To(HaveOccurred()) 127 | }) 128 | }) 129 | 130 | Context("RINQ_PRUNE_INTERVAL", func() { 131 | It("returns a PruneInterval option", func() { 132 | os.Setenv("RINQ_PRUNE_INTERVAL", "1500") 133 | o, err := options.FromEnv() 134 | 135 | Expect(err).NotTo(HaveOccurred()) 136 | 137 | opts, err := options.NewOptions(o...) 138 | 139 | Expect(err).NotTo(HaveOccurred()) 140 | Expect(opts.PruneInterval).To(Equal(1500 * time.Millisecond)) 141 | }) 142 | 143 | It("returns an error if the value is not a positive integer", func() { 144 | os.Setenv("RINQ_PRUNE_INTERVAL", "-500") 145 | _, err := options.FromEnv() 146 | 147 | Expect(err).To(HaveOccurred()) 148 | }) 149 | }) 150 | 151 | Context("RINQ_PRODUCT", func() { 152 | It("returns a Product option", func() { 153 | os.Setenv("RINQ_PRODUCT", "my-app") 154 | o, err := options.FromEnv() 155 | 156 | Expect(err).NotTo(HaveOccurred()) 157 | 158 | opts, err := options.NewOptions(o...) 159 | 160 | Expect(err).NotTo(HaveOccurred()) 161 | Expect(opts.Product).To(Equal("my-app")) 162 | }) 163 | }) 164 | }) 165 | -------------------------------------------------------------------------------- /src/rinq/options/gingo_test.go: -------------------------------------------------------------------------------- 1 | package options_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "options") 13 | } 14 | -------------------------------------------------------------------------------- /src/rinq/options/option.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jmalloc/twelf/src/twelf" 7 | opentracing "github.com/opentracing/opentracing-go" 8 | ) 9 | 10 | // Option is a function that applies a configuration change. 11 | type Option func(v visitor) error 12 | 13 | // DefaultTimeout returns an Option that specifies the maximum amount of time to 14 | // wait for a call to return. It is used if the context passed to Session.Call() 15 | // does not already have a deadline. 16 | func DefaultTimeout(t time.Duration) Option { 17 | return func(v visitor) error { 18 | return v.applyDefaultTimeout(t) 19 | } 20 | } 21 | 22 | // Logger returns an Option that specifies the target for all of the peer's logs. 23 | func Logger(l twelf.Logger) Option { 24 | return func(v visitor) error { 25 | return v.applyLogger(l) 26 | } 27 | } 28 | 29 | // CommandWorkers returns an Option that specifies the number of incoming command 30 | // REQUESTS that are accepted at any given time. A new goroutine is started to 31 | // service each command request. 32 | func CommandWorkers(n uint) Option { 33 | return func(v visitor) error { 34 | return v.applyCommandWorkers(n) 35 | } 36 | } 37 | 38 | // SessionWorkers returns an Option that specifies the number of command RESPONSES 39 | // or notifications that are buffered in memory at any given time. 40 | func SessionWorkers(n uint) Option { 41 | return func(v visitor) error { 42 | return v.applySessionWorkers(n) 43 | } 44 | } 45 | 46 | // PruneInterval returns an Option that specifies how often the cache of remote 47 | // session information is purged of unused data. 48 | func PruneInterval(t time.Duration) Option { 49 | return func(v visitor) error { 50 | return v.applyPruneInterval(t) 51 | } 52 | } 53 | 54 | // Product returns an Option that specifies an application-defined string that 55 | // identifies the application. 56 | // 57 | // It is recommended that the product take the form "/" 58 | // such as "my-app/1.3.0". 59 | func Product(p string) Option { 60 | return func(v visitor) error { 61 | return v.applyProduct(p) 62 | } 63 | } 64 | 65 | // Tracer returns an Option that specifies an OpenTracing tracer to use for 66 | // tracking Rinq operations. 67 | // 68 | // See http://opentracing.io for more information. 69 | func Tracer(t opentracing.Tracer) Option { 70 | return func(v visitor) error { 71 | return v.applyTracer(t) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/rinq/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jmalloc/twelf/src/twelf" 7 | opentracing "github.com/opentracing/opentracing-go" 8 | ) 9 | 10 | // Options is a structure representing a resolved set of options. 11 | type Options struct { 12 | DefaultTimeout time.Duration 13 | Logger twelf.Logger 14 | CommandWorkers uint 15 | SessionWorkers uint 16 | PruneInterval time.Duration 17 | Product string 18 | Tracer opentracing.Tracer 19 | } 20 | 21 | // NewOptions returns a new Options object from the given options, with default 22 | // values for any options that are not specified. 23 | func NewOptions(opts ...Option) (o Options, err error) { 24 | err = Apply(&o, opts...) 25 | return 26 | } 27 | 28 | // applyDefaultTimeout sets the DefaultTimeout value. 29 | func (o *Options) applyDefaultTimeout(v time.Duration) error { 30 | o.DefaultTimeout = v 31 | return nil 32 | } 33 | 34 | // applyLogger sets the Logger value. 35 | func (o *Options) applyLogger(v twelf.Logger) error { 36 | if v == nil { 37 | panic("logger must not be nil") 38 | } 39 | 40 | o.Logger = v 41 | return nil 42 | } 43 | 44 | // applyCommandWorkers sets the CommandWorkers value. 45 | func (o *Options) applyCommandWorkers(v uint) error { 46 | o.CommandWorkers = v 47 | return nil 48 | } 49 | 50 | // applySessionWorkers sets the SessionWorkers value. 51 | func (o *Options) applySessionWorkers(v uint) error { 52 | o.SessionWorkers = v 53 | return nil 54 | } 55 | 56 | // applyPruneInterval sets the PruneInterval value. 57 | func (o *Options) applyPruneInterval(v time.Duration) error { 58 | o.PruneInterval = v 59 | return nil 60 | } 61 | 62 | // applyProduct sets the Product value. 63 | func (o *Options) applyProduct(v string) error { 64 | o.Product = v 65 | return nil 66 | } 67 | 68 | // applyTracer sets the Tracer value. 69 | func (o *Options) applyTracer(v opentracing.Tracer) error { 70 | if v == nil { 71 | panic("tracer must not be nil") 72 | } 73 | 74 | o.Tracer = v 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /src/rinq/options/options_test.go: -------------------------------------------------------------------------------- 1 | package options_test 2 | 3 | import ( 4 | "runtime" 5 | "time" 6 | 7 | "github.com/jmalloc/twelf/src/twelf" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | opentracing "github.com/opentracing/opentracing-go" 11 | "github.com/rinq/rinq-go/src/rinq/options" 12 | ) 13 | 14 | var _ = Describe("NewOptions", func() { 15 | It("uses the correct defaults", func() { 16 | opts, err := options.NewOptions() 17 | 18 | Expect(err).NotTo(HaveOccurred()) 19 | Expect(opts).To(Equal(options.Options{ 20 | DefaultTimeout: 5 * time.Second, 21 | CommandWorkers: uint(runtime.GOMAXPROCS(0)), 22 | SessionWorkers: uint(runtime.GOMAXPROCS(0)) * 10, 23 | Logger: &twelf.StandardLogger{}, 24 | PruneInterval: 3 * time.Minute, 25 | Product: "", 26 | Tracer: opentracing.NoopTracer{}, 27 | })) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/rinq/options/pkg.go: -------------------------------------------------------------------------------- 1 | // Package options defines options that can be customized when creating a Peer. 2 | package options 3 | -------------------------------------------------------------------------------- /src/rinq/options/visitor.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "runtime" 5 | "time" 6 | 7 | "github.com/jmalloc/twelf/src/twelf" 8 | opentracing "github.com/opentracing/opentracing-go" 9 | ) 10 | 11 | // visitor handles the application of options. 12 | type visitor interface { 13 | applyDefaultTimeout(time.Duration) error 14 | applyLogger(twelf.Logger) error 15 | applyCommandWorkers(uint) error 16 | applySessionWorkers(uint) error 17 | applyPruneInterval(time.Duration) error 18 | applyProduct(string) error 19 | applyTracer(opentracing.Tracer) error 20 | } 21 | 22 | // Apply applies the default options, then a sequence of additional options to v. 23 | func Apply(v visitor, opts ...Option) error { 24 | if err := v.applyDefaultTimeout(5 * time.Second); err != nil { 25 | return err 26 | } 27 | 28 | procs := uint(runtime.GOMAXPROCS(0)) 29 | if err := v.applyCommandWorkers(procs); err != nil { 30 | return err 31 | } 32 | 33 | if err := v.applySessionWorkers(procs * 10); err != nil { 34 | return err 35 | } 36 | 37 | if err := v.applyLogger(defaultLogger); err != nil { 38 | return err 39 | } 40 | 41 | if err := v.applyPruneInterval(3 * time.Minute); err != nil { 42 | return err 43 | } 44 | 45 | if err := v.applyTracer(opentracing.NoopTracer{}); err != nil { 46 | return err 47 | } 48 | 49 | for _, o := range opts { 50 | if err := o(v); err != nil { 51 | return err 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | var defaultLogger twelf.Logger 59 | 60 | func init() { 61 | // Initialize the default logger before any testing framework can redirect 62 | // stdout. This lets us use standard "Output:" checks in example tests 63 | // without having to match the log output, while still printing the log 64 | // output in case of a test failure. 65 | defaultLogger = &twelf.StandardLogger{} 66 | } 67 | -------------------------------------------------------------------------------- /src/rinq/peer.go: -------------------------------------------------------------------------------- 1 | package rinq 2 | 3 | import "github.com/rinq/rinq-go/src/rinq/ident" 4 | 5 | // Peer represents a connection to a Rinq network. 6 | // 7 | // Peers can act as a server, responding to application-defined commands. 8 | // Use Peer.Listen() to start accepting incoming command requests. 9 | // 10 | // Command request are sent by sessions, represented by the Session interface. 11 | // Sessions can also send notifications to other sessions. Sessions are created 12 | // by calling Peer.Session(). 13 | // 14 | // Each peer is assigned a unique ID, which is represented by the PeerID struct. 15 | // All IDs generated by the peer, such as session IDs and message IDs contain 16 | // the peer ID, so that they can be traced to their origin easily. 17 | type Peer interface { 18 | // ID returns the peer's unique identifier. 19 | ID() ident.PeerID 20 | 21 | // Session returns a new session owned by this peer. 22 | // 23 | // Creating a session does not perform any network IO. The only limit to the 24 | // number of sessions is the memory required to store them. 25 | // 26 | // Sessions created after the peer has been stopped are unusable. Any 27 | // operation will fail immediately. 28 | Session() Session 29 | 30 | // Listen starts listening for command requests in the given namespace. 31 | // 32 | // When a command request is received with a namespace equal to ns, the 33 | // handler h is invoked. 34 | // 35 | // Repeated calls to Listen() with the same namespace simply changes the 36 | // handler associated with that namespace. 37 | // 38 | // h is invoked on its own goroutine for each command request. 39 | Listen(ns string, h CommandHandler) error 40 | 41 | // Unlisten stops listening for command requests in the given namepsace. 42 | // 43 | // If the peer is not currently listening to ns, nil is returned immediately. 44 | Unlisten(ns string) error 45 | 46 | // Done returns a channel that is closed when the peer is stopped. 47 | // 48 | // Err() may be called to obtain the error that caused the peer to stop, if 49 | // any occurred. 50 | Done() <-chan struct{} 51 | 52 | // Err returns the error that caused the Done() channel to close. 53 | // 54 | // A nil return value indicates that the peer was stopped because Stop() or 55 | // GracefulStop() has been called. 56 | Err() error 57 | 58 | // Stop instructs the peer to disconnect from the network immediately. 59 | // 60 | // Stop does NOT block until the peer is disconnected. Use the Done() 61 | // channel to wait for the peer to disconnect. 62 | Stop() 63 | 64 | // GracefulStop() instructs the peer to disconnect from the network once 65 | // all pending operations have completed. 66 | // 67 | // Any calls to Session.Call(), command handlers or notification handlers 68 | // must return before the peer has stopped. 69 | // 70 | // GracefulStop does NOT block until the peer is disconnected. Use the 71 | // Done() channel to wait for the peer to disconnect. 72 | GracefulStop() 73 | } 74 | -------------------------------------------------------------------------------- /src/rinq/peer_example_test.go: -------------------------------------------------------------------------------- 1 | // +build !without_amqp,!without_examples 2 | 3 | package rinq_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | . "github.com/rinq/rinq-go/src/rinq" 10 | "github.com/rinq/rinq-go/src/rinqamqp" 11 | ) 12 | 13 | // This example illustrates how to establish a new session. 14 | func ExamplePeer_session() { 15 | peer, err := rinqamqp.DialEnv() 16 | if err != nil { 17 | panic(err) 18 | } 19 | defer peer.Stop() 20 | 21 | sess := peer.Session() 22 | defer sess.Destroy() 23 | 24 | fmt.Printf("created session #%d\n", sess.ID().Seq) 25 | // Output: created session #1 26 | } 27 | 28 | // This example illustrates how to listen for incoming command requests. 29 | func ExamplePeer_listen() { 30 | peer, err := rinqamqp.DialEnv() 31 | if err != nil { 32 | panic(err) 33 | } 34 | defer peer.Stop() 35 | 36 | peer.Listen("my-api", func( 37 | ctx context.Context, 38 | req Request, 39 | res Response, 40 | ) { 41 | defer req.Payload.Close() 42 | // handle the command 43 | res.Close() 44 | }) 45 | 46 | if false { // prevent the example from blocking forever. 47 | <-peer.Done() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/rinq/pkg.go: -------------------------------------------------------------------------------- 1 | // Package rinq is a cross-language command bus and distributed ephemeral data store. 2 | package rinq 3 | 4 | // Version is the rinq-go library version. 5 | const Version = "0.7.0" 6 | -------------------------------------------------------------------------------- /src/rinq/pkg_math_example_test.go: -------------------------------------------------------------------------------- 1 | // +build !without_amqp,!without_examples 2 | 3 | package rinq_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/rinq/rinq-go/src/rinq" 10 | "github.com/rinq/rinq-go/src/rinqamqp" 11 | ) 12 | 13 | // arguments contains the parameters for the commands in the "math" namespace 14 | type arguments struct { 15 | Left, Right int 16 | } 17 | 18 | // mathHandler is the command handler for the "math" namespace 19 | func mathHandler( 20 | ctx context.Context, 21 | req rinq.Request, 22 | res rinq.Response, 23 | ) { 24 | defer req.Payload.Close() 25 | 26 | // decode the request payload into the arguments struct 27 | var args arguments 28 | if err := req.Payload.Decode(&args); err != nil { 29 | res.Fail("invalid-arguments", "could not decode arguments") 30 | return 31 | } 32 | 33 | var result int 34 | 35 | switch req.Command { 36 | case "add": 37 | result = args.Left + args.Right 38 | case "sub": 39 | result = args.Left - args.Right 40 | default: 41 | res.Fail("unknown-command", "no such command: "+req.Command) 42 | return 43 | } 44 | 45 | // send the result in the response payload 46 | payload := rinq.NewPayload(result) 47 | defer payload.Close() 48 | 49 | res.Done(payload) 50 | } 51 | 52 | // This example shows how to issue a command call from one peer to another. 53 | // 54 | // There is a "server" peer, which performs basic mathematical operations, 55 | // and a "client" peer which invokes those operations. 56 | // 57 | // In the example both the client peer and the server peer are running in the 58 | // same process. Outside of an example, these peers would typically be running 59 | // on separate servers. 60 | func Example_mathService() { 61 | // create a new peer to act as the "server" and start listening for commands 62 | // in the "math" namespace. 63 | serverPeer, err := rinqamqp.DialEnv() 64 | if err != nil { 65 | panic(err) 66 | } 67 | defer serverPeer.Stop() 68 | serverPeer.Listen("math", mathHandler) 69 | 70 | // create a new peer to act as the "client", and a session to make the 71 | // call. 72 | clientPeer, err := rinqamqp.DialEnv() 73 | if err != nil { 74 | panic(err) 75 | } 76 | defer clientPeer.Stop() 77 | 78 | sess := clientPeer.Session() 79 | defer sess.Destroy() 80 | 81 | // call the "math::add" command 82 | ctx := context.Background() 83 | args := rinq.NewPayload(arguments{1, 2}) 84 | result, err := sess.Call(ctx, "math", "add", args) 85 | if err != nil { 86 | panic(err) 87 | } 88 | 89 | fmt.Printf("1 + 2 = %s\n", result) 90 | // Output: 1 + 2 = 3 91 | } 92 | -------------------------------------------------------------------------------- /src/rinq/revision_example_test.go: -------------------------------------------------------------------------------- 1 | // +build !without_amqp,!without_examples 2 | 3 | package rinq_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | . "github.com/rinq/rinq-go/src/rinq" 10 | "github.com/rinq/rinq-go/src/rinqamqp" 11 | ) 12 | 13 | // This example illustrates how to read an attribute from a session. 14 | // 15 | // It includes logic necessary to fetch the attribute even if the Revision in 16 | // use is out-of-date, by retrying on the latest revision. 17 | func ExampleRevision_get() { 18 | peer, err := rinqamqp.DialEnv() 19 | if err != nil { 20 | panic(err) 21 | } 22 | defer peer.Stop() 23 | 24 | sess := peer.Session() 25 | defer sess.Destroy() 26 | 27 | rev := sess.CurrentRevision() 28 | 29 | ctx := context.Background() 30 | var attr Attr 31 | for { 32 | attr, err = rev.Get(ctx, "my-api", "user-id") 33 | if err != nil { 34 | if ShouldRetry(err) { 35 | // the attribute could not be fetched because it has been 36 | // updated since rev was obtained 37 | rev, err = rev.Refresh(ctx) 38 | if err == nil { 39 | continue 40 | } 41 | } 42 | panic(err) 43 | } 44 | 45 | break 46 | } 47 | 48 | if attr.Value == "" { 49 | fmt.Println("user is not logged in") 50 | } 51 | 52 | // Output: user is not logged in 53 | } 54 | 55 | // This example illustrates how to modify an attribute in a session. 56 | // 57 | // It includes logic to retry in the face of an optimistic-lock failure, which 58 | // occurs if the revision is out of date. 59 | func ExampleRevision_update() { 60 | peer, err := rinqamqp.DialEnv() 61 | if err != nil { 62 | panic(err) 63 | } 64 | defer peer.Stop() 65 | 66 | sess := peer.Session() 67 | defer sess.Destroy() 68 | 69 | rev := sess.CurrentRevision() 70 | 71 | ctx := context.Background() 72 | 73 | for { 74 | rev, err = rev.Update(ctx, "my-api", Set("user-id", "123")) 75 | if err != nil { 76 | if ShouldRetry(err) { 77 | // the session could not be updated because rev is out of date 78 | rev, err = rev.Refresh(ctx) 79 | if err == nil { 80 | continue 81 | } 82 | } 83 | panic(err) 84 | } 85 | 86 | fmt.Println("updated to new revision") 87 | break 88 | } 89 | 90 | // Output: updated to new revision 91 | } 92 | -------------------------------------------------------------------------------- /src/rinq/revision_test.go: -------------------------------------------------------------------------------- 1 | package rinq_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | "github.com/rinq/rinq-go/src/rinq" 7 | "github.com/rinq/rinq-go/src/rinq/ident" 8 | ) 9 | 10 | var _ = Describe("Revision", func() { 11 | var sessionRef = ident.Ref{ 12 | ID: ident.SessionID{ 13 | Peer: ident.PeerID{ 14 | Clock: 1, 15 | Rand: 2, 16 | }, 17 | Seq: 3, 18 | }, 19 | Rev: 4, 20 | } 21 | 22 | Describe("ShouldRetry", func() { 23 | It("returns true for StaleFetchError", func() { 24 | r := rinq.ShouldRetry(rinq.StaleFetchError{}) 25 | Expect(r).To(BeTrue()) 26 | }) 27 | 28 | It("returns true for StaleUpdateError", func() { 29 | r := rinq.ShouldRetry(rinq.StaleUpdateError{}) 30 | Expect(r).To(BeTrue()) 31 | }) 32 | 33 | It("returns false for other error types", func() { 34 | r := rinq.ShouldRetry(rinq.FrozenAttributesError{}) 35 | Expect(r).To(BeFalse()) 36 | }) 37 | }) 38 | 39 | Describe("StaleFetchError", func() { 40 | Describe("Error", func() { 41 | It("returns the message", func() { 42 | err := rinq.StaleFetchError{Ref: sessionRef} 43 | Expect(err.Error()).To(Equal( 44 | "can not fetch attributes at 1-0002.3@4, one or more attributes have been modified since that revision", 45 | )) 46 | }) 47 | }) 48 | }) 49 | 50 | Describe("StaleUpdateError", func() { 51 | Describe("Error", func() { 52 | It("returns the message", func() { 53 | err := rinq.StaleUpdateError{Ref: sessionRef} 54 | Expect(err.Error()).To(Equal( 55 | "can not update or close 1-0002.3@4, the session has been modified since that revision", 56 | )) 57 | }) 58 | }) 59 | }) 60 | 61 | Describe("FrozenAttributesError", func() { 62 | Describe("Error", func() { 63 | It("returns the message", func() { 64 | err := rinq.FrozenAttributesError{Ref: sessionRef} 65 | Expect(err.Error()).To(Equal( 66 | "can not update 1-0002.3@4, the change affects one or more frozen attributes", 67 | )) 68 | }) 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /src/rinq/session_test.go: -------------------------------------------------------------------------------- 1 | package rinq_test 2 | 3 | import ( 4 | "errors" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | "github.com/rinq/rinq-go/src/rinq" 9 | "github.com/rinq/rinq-go/src/rinq/ident" 10 | ) 11 | 12 | var _ = Describe("NotFoundError", func() { 13 | Describe("Error", func() { 14 | It("includes the session ID", func() { 15 | id := ident.SessionID{ 16 | Peer: ident.PeerID{Clock: 1, Rand: 2}, 17 | Seq: 3, 18 | } 19 | err := rinq.NotFoundError{ID: id} 20 | Expect(err.Error()).To(Equal("session 1-0002.3 not found")) 21 | }) 22 | }) 23 | 24 | Describe("IsNotFound", func() { 25 | It("returns true for not found errors", func() { 26 | Expect(rinq.IsNotFound(rinq.NotFoundError{})).To(BeTrue()) 27 | }) 28 | 29 | It("returns false for other error types", func() { 30 | Expect(rinq.IsNotFound(errors.New(""))).To(BeFalse()) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/rinq/trace/context.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import "context" 4 | 5 | // With returns a new context derived from parent that includes an 6 | // application-defined "trace ID" value. 7 | // 8 | // Any operations (such as command calls, session notifications, etc) that use 9 | // the returned context will include the trace ID in log output on both the 10 | // sending and receiving peer. 11 | // 12 | // The trace ID is also present in the ctx supplied to command and notification 13 | // handlers. This allows very easy propagation of the trace identifier to all 14 | // "sub-requests" of the initial operation. 15 | // 16 | // The trace ID is rendered surrounded by square brackets in log output. 17 | // 18 | // If an operation is performed with ctx that does not contain a trace ID, 19 | // the operation's message ID is used. This includes sufficient information to 20 | // identify the peer, session and revision of the initial operation. 21 | func With(parent context.Context, t string) context.Context { 22 | return context.WithValue(parent, key, t) 23 | } 24 | 25 | // WithRoot returns a new context derived from parent that includes an 26 | // application-defined "trace ID" value, only if the parent does not already 27 | // contain such a trace ID. 28 | // 29 | // If parent already contains a trace ID, ctx is parent, id is the trace ID from 30 | // parent and ok is false. Otherwise, ctx is the derived context containing t 31 | // as the trace ID, id is t and ok is true. 32 | func WithRoot(parent context.Context, t string) (ctx context.Context, id string, ok bool) { 33 | existing := parent.Value(key) 34 | 35 | if existing == nil { 36 | return With(parent, t), t, true 37 | } 38 | 39 | return parent, existing.(string), false 40 | } 41 | 42 | // Get returns the trace identifier from ctx, or an empty string if none is 43 | // present. 44 | func Get(ctx context.Context) string { 45 | str, _ := ctx.Value(key).(string) 46 | return str 47 | } 48 | 49 | type keyType struct{} 50 | 51 | var key keyType 52 | -------------------------------------------------------------------------------- /src/rinq/trace/context_test.go: -------------------------------------------------------------------------------- 1 | package trace_test 2 | 3 | import ( 4 | "context" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | . "github.com/rinq/rinq-go/src/rinq/trace" 9 | ) 10 | 11 | var _ = Describe("With", func() { 12 | It("adds the trace ID", func() { 13 | ctx := With(context.Background(), "") 14 | 15 | Expect(Get(ctx)).To(Equal("")) 16 | }) 17 | 18 | It("replaces an existing trace ID", func() { 19 | parent := With(context.Background(), "") 20 | ctx := With(parent, "") 21 | 22 | Expect(Get(ctx)).To(Equal("")) 23 | }) 24 | }) 25 | 26 | var _ = Describe("WithRoot", func() { 27 | It("adds the trace ID", func() { 28 | ctx, id, added := WithRoot(context.Background(), "") 29 | 30 | Expect(Get(ctx)).To(Equal("")) 31 | Expect(id).To(Equal("")) 32 | Expect(added).To(BeTrue()) 33 | }) 34 | 35 | It("returns the parent context unchanged", func() { 36 | parent := With(context.Background(), "") 37 | ctx, id, added := WithRoot(parent, "") 38 | 39 | Expect(ctx).To(BeIdenticalTo(parent)) 40 | Expect(id).To(Equal("")) 41 | Expect(added).To(BeFalse()) 42 | }) 43 | }) 44 | 45 | var _ = Describe("Get", func() { 46 | It("returns an empty string when no trace ID is present", func() { 47 | ctx := context.Background() 48 | 49 | Expect(Get(ctx)).To(Equal("")) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/rinq/trace/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package trace_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "trace") 13 | } 14 | -------------------------------------------------------------------------------- /src/rinq/trace/pkg.go: -------------------------------------------------------------------------------- 1 | // Package trace provides functions for configuring custom trace identifiers. 2 | package trace 3 | -------------------------------------------------------------------------------- /src/rinqamqp/dialer_example_test.go: -------------------------------------------------------------------------------- 1 | // +build !without_amqp,!without_examples 2 | 3 | package rinqamqp_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/rinq/rinq-go/src/rinq/options" 11 | . "github.com/rinq/rinq-go/src/rinqamqp" 12 | ) 13 | 14 | // This example demonstrates how to establish a peer on a Rinq network using 15 | // the default configuration. 16 | func ExampleDial() { 17 | peer, err := Dial("amqp://localhost") 18 | if err != nil { 19 | panic(err) 20 | } 21 | defer peer.Stop() 22 | 23 | fmt.Printf("connected") 24 | // Output: connected 25 | } 26 | 27 | // This example demonstrates how to establish a peer on a Rinq network using 28 | // custom options. 29 | func ExampleDial_withOptions() { 30 | peer, err := Dial( 31 | "amqp://localhost", 32 | options.DefaultTimeout(10*time.Second), 33 | ) 34 | if err != nil { 35 | panic(err) 36 | } 37 | defer peer.Stop() 38 | 39 | fmt.Println("connected") 40 | // Output: connected 41 | } 42 | 43 | // This example demonstrates how to establish a peer on a Rinq network using a 44 | // Dialer with a custom AMQP configuration. 45 | func ExampleDialer() { 46 | dialer := &Dialer{} 47 | dialer.AMQPConfig.Heartbeat = 1 * time.Minute 48 | 49 | peer, err := dialer.Dial( 50 | context.Background(), 51 | "amqp://localhost", 52 | ) 53 | if err != nil { 54 | panic(err) 55 | } 56 | defer peer.Stop() 57 | 58 | fmt.Println("connected") 59 | // Output: connected 60 | } 61 | -------------------------------------------------------------------------------- /src/rinqamqp/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package rinqamqp_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "amqp") 13 | } 14 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/amqputil/channel_pool.go: -------------------------------------------------------------------------------- 1 | package amqputil 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/streadway/amqp" 7 | ) 8 | 9 | const maxPreFetch = ^uint(0) >> 1 // largest int value as uint 10 | 11 | // ChannelPool provides a pool of reusable AMQP channels. 12 | type ChannelPool interface { 13 | // Get fetches a channel from the pool, or creates one as necessary. 14 | Get() (*amqp.Channel, error) 15 | 16 | // GetQOS fetches a channel from the pool and sets the pre-fetch count 17 | // before returning it. The pre-fetch is applied to across all consumers on 18 | // the channel. 19 | GetQOS(preFetch uint) (*amqp.Channel, error) 20 | 21 | // Put returns a channel to the pool. 22 | Put(*amqp.Channel) 23 | } 24 | 25 | // NewChannelPool returns a channel pool of the given size. 26 | func NewChannelPool(broker *amqp.Connection, size uint) ChannelPool { 27 | return &channelPool{ 28 | broker: broker, 29 | channels: make(chan *amqp.Channel, size), 30 | } 31 | } 32 | 33 | type channelPool struct { 34 | broker *amqp.Connection 35 | channels chan *amqp.Channel 36 | } 37 | 38 | func (p *channelPool) Get() (channel *amqp.Channel, err error) { 39 | select { 40 | case channel = <-p.channels: // fetch from the pool 41 | default: // none available, make a new channel 42 | channel, err = p.broker.Channel() 43 | } 44 | 45 | return 46 | } 47 | 48 | // GetQOS fetches a channel from the pool and sets the pre-fetch count 49 | // before returning it. The pre-fetch is applied across all consumers on 50 | // the channel. 51 | func (p *channelPool) GetQOS(preFetch uint) (*amqp.Channel, error) { 52 | channel, err := p.Get() 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // Always use a "channel-wide" QoS setting. 58 | // http://www.rabbitmq.com/consumer-prefetch.html 59 | caps, _ := p.broker.Properties["capabilities"].(amqp.Table) 60 | global, _ := caps["per_consumer_qos"].(bool) 61 | 62 | if preFetch > maxPreFetch { 63 | return nil, errors.New("pre-fetch is too large") 64 | } 65 | 66 | err = channel.Qos(int(preFetch), 0, global) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | return channel, nil 72 | } 73 | 74 | func (p *channelPool) Put(channel *amqp.Channel) { 75 | if channel == nil { 76 | return 77 | } 78 | 79 | // set the QoS state back to unlimited, both to "reset" the channel, and to 80 | // verify that it is still usable. 81 | if err := channel.Qos(0, 0, true); err != nil { 82 | return 83 | } 84 | 85 | select { 86 | case p.channels <- channel: // return to the pool 87 | default: // pool is full, close channel 88 | _ = channel.Close() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/amqputil/deadline.go: -------------------------------------------------------------------------------- 1 | package amqputil 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/streadway/amqp" 9 | ) 10 | 11 | // PackDeadline uses deadline information from ctx to populate the expiration 12 | // field of msg. The return value is true if a deadline is present. 13 | // 14 | // The context "done" error is returned if the deadline has already passed or 15 | // the context has been canceled. 16 | func PackDeadline(ctx context.Context, msg *amqp.Publishing) (bool, error) { 17 | deadline, ok := ctx.Deadline() 18 | if !ok { 19 | return false, nil 20 | } 21 | 22 | if msg.Headers == nil { 23 | msg.Headers = amqp.Table{} 24 | } 25 | 26 | // calculate the deadline and store it in a header 27 | deadlineNanos := deadline.UnixNano() 28 | deadlineMillis := deadlineNanos / int64(time.Millisecond) 29 | msg.Headers[deadlineHeader] = deadlineMillis 30 | 31 | // calculate the expiration based on current time 32 | msg.Expiration = "0" 33 | remainingMillis := time.Until(deadline) / time.Millisecond 34 | 35 | select { 36 | case <-ctx.Done(): 37 | return true, ctx.Err() 38 | default: 39 | if remainingMillis > 0 { 40 | msg.Expiration = strconv.FormatInt(int64(remainingMillis), 10) 41 | } 42 | return true, nil 43 | } 44 | } 45 | 46 | // UnpackDeadline creates a new context based on parent which has a deadline 47 | // computed from the expiration information in msg. 48 | // 49 | // The return values are the same as context.WithDeadline() 50 | func UnpackDeadline(parent context.Context, msg *amqp.Delivery) (context.Context, func()) { 51 | deadlineMillis, ok := msg.Headers[deadlineHeader].(int64) 52 | if !ok { 53 | return context.WithCancel(parent) 54 | } 55 | 56 | deadlineNanos := deadlineMillis * int64(time.Millisecond) 57 | deadline := time.Unix(0, deadlineNanos) 58 | 59 | return context.WithDeadline(parent, deadline) 60 | } 61 | 62 | const deadlineHeader = "dl" 63 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/amqputil/deadline_test.go: -------------------------------------------------------------------------------- 1 | package amqputil_test 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "time" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | "github.com/rinq/rinq-go/src/rinqamqp/internal/amqputil" 11 | "github.com/streadway/amqp" 12 | ) 13 | 14 | var _ = Describe("Deadline", func() { 15 | Describe("PackDeadline", func() { 16 | It("sets the expiration", func() { 17 | now := time.Now() 18 | deadline := now.Add(10 * time.Second) 19 | 20 | ctx, cancel := context.WithDeadline(context.Background(), deadline) 21 | defer cancel() 22 | 23 | msg := amqp.Publishing{} 24 | hasDeadline, err := amqputil.PackDeadline(ctx, &msg) 25 | 26 | Expect(err).ShouldNot(HaveOccurred()) 27 | Expect(hasDeadline).To(BeTrue()) 28 | 29 | expiration, err := strconv.ParseUint(msg.Expiration, 10, 64) 30 | Expect(err).ShouldNot(HaveOccurred()) 31 | 32 | Expect(expiration).Should(BeNumerically("~", (10*time.Second)/time.Millisecond, 10)) 33 | Expect(msg.Headers["dl"].(int64)).To(Equal(deadline.UnixNano() / int64(time.Millisecond))) 34 | }) 35 | 36 | It("returns an error if the deadline has already passed", func() { 37 | ctx, cancel := context.WithTimeout(context.Background(), -1) 38 | defer cancel() 39 | 40 | msg := amqp.Publishing{} 41 | hasDeadline, err := amqputil.PackDeadline(ctx, &msg) 42 | 43 | Expect(err).To(Equal(ctx.Err())) 44 | Expect(hasDeadline).To(BeTrue()) 45 | }) 46 | 47 | It("does nothing if the context has no deadline", func() { 48 | msg := amqp.Publishing{} 49 | hasDeadline, err := amqputil.PackDeadline(context.Background(), &msg) 50 | 51 | Expect(err).ShouldNot(HaveOccurred()) 52 | Expect(hasDeadline).To(BeFalse()) 53 | 54 | Expect(msg.Expiration).To(Equal("")) 55 | Expect(msg.Timestamp.IsZero()).To(BeTrue()) 56 | }) 57 | }) 58 | 59 | Describe("UnpackDeadline", func() { 60 | It("returns a context with the deadline from the message", func() { 61 | expected := time.Now() 62 | 63 | msg := amqp.Delivery{ 64 | Headers: amqp.Table{"dl": expected.UnixNano() / int64(time.Millisecond)}, 65 | Expiration: "0", 66 | } 67 | 68 | ctx, cancel := amqputil.UnpackDeadline(context.Background(), &msg) 69 | defer cancel() 70 | 71 | deadline, ok := ctx.Deadline() 72 | 73 | Expect(ok).To(BeTrue()) 74 | Expect(deadline).To(BeTemporally("~", expected, time.Millisecond)) // within one milli 75 | }) 76 | 77 | It("does not add a deadline if there is no deadline in the message", func() { 78 | msg := amqp.Delivery{ 79 | Expiration: "1000", 80 | } 81 | 82 | ctx, cancel := amqputil.UnpackDeadline(context.Background(), &msg) 83 | defer cancel() 84 | 85 | _, ok := ctx.Deadline() 86 | 87 | Expect(ok).To(BeFalse()) 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/amqputil/ginkgo_test.go: -------------------------------------------------------------------------------- 1 | package amqputil_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo" 7 | "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "internal") 13 | } 14 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/amqputil/span.go: -------------------------------------------------------------------------------- 1 | package amqputil 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | 8 | opentracing "github.com/opentracing/opentracing-go" 9 | "github.com/rinq/rinq-go/src/internal/x/bufferpool" 10 | "github.com/streadway/amqp" 11 | ) 12 | 13 | const ( 14 | // spanContextHeader contains the serialied OpenTracing span context. 15 | spanContextHeader = "sc" 16 | ) 17 | 18 | // PackSpanContext packs a serialized "span context" into the headers of msg 19 | // based on the span in ctx, if any. 20 | func PackSpanContext(ctx context.Context, msg *amqp.Publishing) error { 21 | span := opentracing.SpanFromContext(ctx) 22 | if span == nil { 23 | return nil 24 | } 25 | 26 | buf := bufferpool.Get() 27 | 28 | if err := span.Tracer().Inject( 29 | span.Context(), 30 | opentracing.Binary, 31 | buf, 32 | ); err != nil { 33 | return err 34 | } 35 | 36 | if buf.Len() > 0 { 37 | if msg.Headers == nil { 38 | msg.Headers = amqp.Table{} 39 | } 40 | 41 | msg.Headers[spanContextHeader] = buf.Bytes() 42 | } 43 | 44 | return nil 45 | } 46 | 47 | // UnpackSpanContext extracts a span context from the headers of msg. If no 48 | // span context is packed in the headers, nil is returned. 49 | func UnpackSpanContext(msg *amqp.Delivery, t opentracing.Tracer) (opentracing.SpanContext, error) { 50 | v, ok := msg.Headers[spanContextHeader] 51 | if !ok { 52 | return nil, nil 53 | } 54 | 55 | b, ok := v.([]byte) 56 | if !ok { 57 | return nil, errors.New("span context header is not a byte slice") 58 | } 59 | 60 | buf := bytes.NewBuffer(b) 61 | defer bufferpool.Put(buf) 62 | 63 | sc, err := t.Extract(opentracing.Binary, buf) 64 | 65 | if err == opentracing.ErrSpanContextNotFound { 66 | return nil, nil 67 | } 68 | 69 | return sc, err 70 | } 71 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/amqputil/trace.go: -------------------------------------------------------------------------------- 1 | package amqputil 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rinq/rinq-go/src/rinq/trace" 7 | "github.com/streadway/amqp" 8 | ) 9 | 10 | // PackTrace sets msg.CorrelationId to traceID, only if it differs to msgID. 11 | // 12 | // The AMQP correlation ID field is used to tie "root" requests (be they command 13 | // requests or notifications) to any requests that are made in response to that 14 | // "root" request. This is different to the popular use of the correlation ID 15 | // field, which is often used to relate a response to a request. 16 | func PackTrace(msg *amqp.Publishing, traceID string) { 17 | if traceID != msg.MessageId { 18 | msg.CorrelationId = traceID 19 | } 20 | } 21 | 22 | // UnpackTrace creates a new context with a trace ID based on the AMQP correlation 23 | // ID from msg. 24 | // 25 | // If the correlation ID is empty, the message is considered a "root" request, 26 | // so the message ID is used as the correlation ID. 27 | func UnpackTrace(parent context.Context, msg *amqp.Delivery) context.Context { 28 | if msg.CorrelationId != "" { 29 | return trace.With(parent, msg.CorrelationId) 30 | } 31 | 32 | return trace.With(parent, msg.MessageId) 33 | } 34 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/amqputil/trace_test.go: -------------------------------------------------------------------------------- 1 | package amqputil_test 2 | 3 | import ( 4 | "context" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | "github.com/rinq/rinq-go/src/rinq/trace" 9 | "github.com/rinq/rinq-go/src/rinqamqp/internal/amqputil" 10 | "github.com/streadway/amqp" 11 | ) 12 | 13 | var _ = Describe("Trace", func() { 14 | Describe("PackTrace", func() { 15 | It("sets the correlation ID", func() { 16 | pub := amqp.Publishing{MessageId: ""} 17 | amqputil.PackTrace(&pub, "") 18 | 19 | Expect(pub.CorrelationId).To(Equal("")) 20 | }) 21 | 22 | It("does not set the correlation ID if the trace ID the same as the message ID", func() { 23 | pub := amqp.Publishing{MessageId: ""} 24 | amqputil.PackTrace(&pub, "") 25 | 26 | Expect(pub.CorrelationId).To(Equal("")) 27 | }) 28 | }) 29 | 30 | Describe("UnpackTrace", func() { 31 | It("returns a context with the trace ID based on the message ID", func() { 32 | del := amqp.Delivery{MessageId: ""} 33 | ctx := amqputil.UnpackTrace(context.Background(), &del) 34 | 35 | Expect(trace.Get(ctx)).To(Equal("")) 36 | }) 37 | 38 | It("returns a context with the trace ID based on the correlation ID", func() { 39 | del := amqp.Delivery{ 40 | MessageId: "", 41 | CorrelationId: "", 42 | } 43 | ctx := amqputil.UnpackTrace(context.Background(), &del) 44 | 45 | Expect(trace.Get(ctx)).To(Equal("")) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/commandamqp/debug_response.go: -------------------------------------------------------------------------------- 1 | package commandamqp 2 | 3 | import "github.com/rinq/rinq-go/src/rinq" 4 | 5 | // debugResponse wraps are "parent" response and captures the payload and error. 6 | type debugResponse struct { 7 | res rinq.Response 8 | 9 | Payload *rinq.Payload 10 | Err error 11 | } 12 | 13 | func newDebugResponse(parent rinq.Response) rinq.Response { 14 | return &debugResponse{ 15 | res: parent, 16 | } 17 | } 18 | 19 | func (r *debugResponse) IsRequired() bool { 20 | return r.res.IsRequired() 21 | } 22 | 23 | func (r *debugResponse) IsClosed() bool { 24 | return r.res.IsClosed() 25 | } 26 | 27 | func (r *debugResponse) Done(payload *rinq.Payload) { 28 | r.res.Done(payload) 29 | r.Payload = payload.Clone() 30 | } 31 | 32 | func (r *debugResponse) Error(err error) { 33 | r.res.Error(err) 34 | r.Err = err 35 | if failure, ok := err.(rinq.Failure); ok { 36 | r.Payload = failure.Payload.Clone() 37 | } 38 | } 39 | 40 | func (r *debugResponse) Fail(t, f string, v ...interface{}) rinq.Failure { 41 | err := r.res.Fail(t, f, v...) 42 | r.Err = err 43 | return err 44 | } 45 | 46 | func (r *debugResponse) Close() bool { 47 | return r.res.Close() 48 | } 49 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/commandamqp/exchanges.go: -------------------------------------------------------------------------------- 1 | package commandamqp 2 | 3 | import "github.com/streadway/amqp" 4 | 5 | const ( 6 | // unicastExchange is the exchange used to publish internal command requests 7 | // directly to a specific peer. 8 | unicastExchange = "cmd.uc" 9 | 10 | // multicastExchange is the exchange used to publish comman requests to the 11 | // all peers that can service the namespace. 12 | multicastExchange = "cmd.mc" 13 | 14 | // balancedExchange is the exchange used publish command requests to the 15 | // first available peer that can service the namespace. 16 | balancedExchange = "cmd.bal" 17 | 18 | // responseExchange is the exchange used to publish command responses. 19 | responseExchange = "cmd.rsp" 20 | ) 21 | 22 | func declareExchanges(channel *amqp.Channel) error { 23 | if err := channel.ExchangeDeclare( 24 | unicastExchange, 25 | "direct", 26 | false, // durable 27 | false, // autoDelete 28 | false, // internal 29 | false, // noWait 30 | nil, // args 31 | ); err != nil { 32 | return err 33 | } 34 | 35 | if err := channel.ExchangeDeclare( 36 | multicastExchange, 37 | "direct", 38 | false, // durable 39 | false, // autoDelete 40 | false, // internal 41 | false, // noWait 42 | nil, // args 43 | ); err != nil { 44 | return err 45 | } 46 | 47 | if err := channel.ExchangeDeclare( 48 | balancedExchange, 49 | "direct", 50 | false, // durable 51 | false, // autoDelete 52 | false, // internal 53 | false, // noWait 54 | nil, // args 55 | ); err != nil { 56 | return err 57 | } 58 | 59 | if err := channel.ExchangeDeclare( 60 | responseExchange, 61 | "topic", 62 | false, // durable 63 | false, // autoDelete 64 | false, // internal 65 | false, // noWait 66 | nil, // args 67 | ); err != nil { 68 | return err 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/commandamqp/factory.go: -------------------------------------------------------------------------------- 1 | package commandamqp 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/internal/command" 5 | "github.com/rinq/rinq-go/src/internal/localsession" 6 | "github.com/rinq/rinq-go/src/internal/revisions" 7 | "github.com/rinq/rinq-go/src/rinq/ident" 8 | "github.com/rinq/rinq-go/src/rinq/options" 9 | "github.com/rinq/rinq-go/src/rinqamqp/internal/amqputil" 10 | ) 11 | 12 | // New returns a pair of invoker and server. 13 | func New( 14 | peerID ident.PeerID, 15 | opts options.Options, 16 | sessions *localsession.Store, 17 | revs revisions.Store, 18 | channels amqputil.ChannelPool, 19 | ) (command.Invoker, command.Server, error) { 20 | channel, err := channels.Get() 21 | if err != nil { 22 | return nil, nil, err 23 | } 24 | defer channels.Put(channel) 25 | 26 | if err = declareExchanges(channel); err != nil { 27 | return nil, nil, err 28 | } 29 | 30 | queues := &queueSet{} 31 | 32 | invoker, err := newInvoker( 33 | peerID, 34 | opts.SessionWorkers, 35 | opts.DefaultTimeout, 36 | sessions, 37 | queues, 38 | channels, 39 | opts.Logger, 40 | opts.Tracer, 41 | ) 42 | if err != nil { 43 | return nil, nil, err 44 | } 45 | 46 | server, err := newServer( 47 | peerID, 48 | opts.CommandWorkers, 49 | revs, 50 | queues, 51 | channels, 52 | opts.Logger, 53 | opts.Tracer, 54 | ) 55 | if err != nil { 56 | invoker.Stop() 57 | <-invoker.Done() 58 | return nil, nil, err 59 | } 60 | 61 | return invoker, server, nil 62 | } 63 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/commandamqp/priority.go: -------------------------------------------------------------------------------- 1 | package commandamqp 2 | 3 | const ( 4 | // executePriority is the AMQP priority for "Execute*" operations. 5 | executePriority uint8 = iota 6 | 7 | // callBalancedPriority is the AMQP priority for "CallBalanced" operations. 8 | // These operations always have a timeout, so the priority is raised above 9 | // operations that don't. 10 | callBalancedPriority 11 | 12 | // callUnicastPriority is the AMQP priority for "CallUnicast" operations. 13 | // Like CallBalanced, these operations have a timeout. They are also used 14 | // to implement internal features, and so the priority is raised even higher 15 | // again. 16 | callUnicastPriority 17 | 18 | // priorityCount is the number of priorities in use, used to declare the 19 | // AMQP queues with the exact number of priority slots. 20 | priorityCount 21 | ) 22 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/commandamqp/queues.go: -------------------------------------------------------------------------------- 1 | package commandamqp 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/rinq/rinq-go/src/rinq/ident" 7 | "github.com/streadway/amqp" 8 | ) 9 | 10 | // balancedRequestQueue returns the name of the queue used for balanced 11 | // command requests in the given namespace. 12 | func balancedRequestQueue(namespace string) string { 13 | return "cmd." + namespace 14 | } 15 | 16 | // requestQueue returns the name of the queue used for unicast and multicast 17 | // command requests. 18 | func requestQueue(id ident.PeerID) string { 19 | return id.ShortString() + ".req" 20 | } 21 | 22 | // responseQueue returns the name of the queue used for command responses. 23 | func responseQueue(id ident.PeerID) string { 24 | return id.ShortString() + ".rsp" 25 | } 26 | 27 | // queueSet declares AMQP resources for queuing balanced command requests. 28 | type queueSet struct { 29 | mutex sync.Mutex 30 | queues map[string]string 31 | } 32 | 33 | // Get declares the AMQP queue used for balanced command requests in the given 34 | // namespace and returns the queue name. 35 | func (s *queueSet) Get(channel *amqp.Channel, namespace string) (string, error) { 36 | s.mutex.Lock() 37 | defer s.mutex.Unlock() 38 | 39 | if queue, ok := s.queues[namespace]; ok { 40 | return queue, nil 41 | } 42 | 43 | queue := balancedRequestQueue(namespace) 44 | 45 | if _, err := channel.QueueDeclare( 46 | queue, 47 | true, // durable 48 | false, // autoDelete 49 | false, // exclusive, 50 | false, // noWait 51 | amqp.Table{"x-max-priority": priorityCount}, 52 | ); err != nil { 53 | return "", err 54 | } 55 | 56 | if err := channel.QueueBind( 57 | queue, 58 | namespace, 59 | balancedExchange, 60 | false, // noWait 61 | nil, // args 62 | ); err != nil { 63 | return "", err 64 | } 65 | 66 | if s.queues == nil { 67 | s.queues = map[string]string{} 68 | } 69 | s.queues[namespace] = queue 70 | 71 | return queue, nil 72 | } 73 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/commandamqp/response.go: -------------------------------------------------------------------------------- 1 | package commandamqp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/rinq/rinq-go/src/rinq" 9 | "github.com/rinq/rinq-go/src/rinq/trace" 10 | "github.com/rinq/rinq-go/src/rinqamqp/internal/amqputil" 11 | "github.com/streadway/amqp" 12 | ) 13 | 14 | // response is used to send responses to command requests, it implements 15 | // rinq.Response. 16 | type response struct { 17 | context context.Context 18 | channels amqputil.ChannelPool 19 | request rinq.Request 20 | 21 | mutex sync.RWMutex 22 | replyMode replyMode 23 | isClosed bool 24 | } 25 | 26 | func newResponse( 27 | ctx context.Context, 28 | channels amqputil.ChannelPool, 29 | request rinq.Request, 30 | replyMode replyMode, 31 | ) (rinq.Response, func() bool) { 32 | r := &response{ 33 | context: ctx, 34 | channels: channels, 35 | request: request, 36 | replyMode: replyMode, 37 | } 38 | 39 | return r, r.finalize 40 | } 41 | 42 | func (r *response) IsRequired() bool { 43 | r.mutex.RLock() 44 | defer r.mutex.RUnlock() 45 | 46 | if r.isClosed { 47 | return false 48 | } 49 | 50 | if r.replyMode == replyNone { 51 | return false 52 | } 53 | 54 | select { 55 | case <-r.context.Done(): 56 | return false 57 | default: 58 | return true 59 | } 60 | } 61 | 62 | func (r *response) IsClosed() bool { 63 | r.mutex.RLock() 64 | defer r.mutex.RUnlock() 65 | 66 | return r.isClosed 67 | } 68 | 69 | func (r *response) Done(payload *rinq.Payload) { 70 | r.mutex.Lock() 71 | defer r.mutex.Unlock() 72 | 73 | if r.isClosed { 74 | panic("responder is already closed") 75 | } 76 | 77 | msg := &amqp.Publishing{} 78 | packSuccessResponse(msg, payload) 79 | r.respond(msg) 80 | } 81 | 82 | func (r *response) Error(err error) { 83 | r.mutex.Lock() 84 | defer r.mutex.Unlock() 85 | 86 | if r.isClosed { 87 | panic("responder is already closed") 88 | } 89 | 90 | msg := &amqp.Publishing{} 91 | packErrorResponse(msg, err) 92 | r.respond(msg) 93 | } 94 | 95 | func (r *response) Fail(t, f string, v ...interface{}) rinq.Failure { 96 | err := rinq.Failure{ 97 | Type: t, 98 | Message: fmt.Sprintf(f, v...), 99 | } 100 | 101 | r.Error(err) 102 | 103 | return err 104 | } 105 | 106 | func (r *response) Close() bool { 107 | r.mutex.Lock() 108 | defer r.mutex.Unlock() 109 | 110 | if r.isClosed { 111 | return false 112 | } 113 | 114 | msg := &amqp.Publishing{} 115 | packSuccessResponse(msg, nil) 116 | r.respond(msg) 117 | 118 | return true 119 | } 120 | 121 | func (r *response) finalize() bool { 122 | r.mutex.Lock() 123 | defer r.mutex.Unlock() 124 | 125 | if r.isClosed { 126 | return true 127 | } 128 | 129 | r.isClosed = true 130 | 131 | return false 132 | } 133 | 134 | func (r *response) respond(msg *amqp.Publishing) { 135 | r.isClosed = true 136 | 137 | if r.replyMode == replyNone { 138 | return 139 | } 140 | 141 | if _, err := amqputil.PackDeadline(r.context, msg); err != nil { 142 | // the context deadline has already passed 143 | return 144 | } 145 | 146 | channel, err := r.channels.Get() 147 | if err != nil { 148 | panic(err) 149 | } 150 | defer r.channels.Put(channel) 151 | 152 | // TODO: is this necessary for correlated responses? 153 | amqputil.PackTrace(msg, trace.Get(r.context)) 154 | 155 | if r.replyMode == replyUncorrelated { 156 | packNamespaceAndCommand(msg, r.request.Namespace, r.request.Command) 157 | packReplyMode(msg, r.replyMode) 158 | 159 | err = amqputil.PackSpanContext(r.context, msg) 160 | if err != nil { 161 | panic(err) 162 | } 163 | } 164 | 165 | err = channel.Publish( 166 | responseExchange, 167 | r.request.ID.String(), 168 | false, // mandatory, 169 | false, // immediate, 170 | *msg, 171 | ) 172 | if err != nil { 173 | panic(err) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/commandamqp/server_logging.go: -------------------------------------------------------------------------------- 1 | package commandamqp 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmalloc/twelf/src/twelf" 7 | "github.com/rinq/rinq-go/src/rinq" 8 | "github.com/rinq/rinq-go/src/rinq/ident" 9 | "github.com/rinq/rinq-go/src/rinq/trace" 10 | ) 11 | 12 | func logServerInvalidMessageID( 13 | logger twelf.Logger, 14 | peerID ident.PeerID, 15 | msgID string, 16 | ) { 17 | logger.Debug( 18 | "%s server ignored AMQP message, '%s' is not a valid message ID", 19 | peerID.ShortString(), 20 | msgID, 21 | ) 22 | } 23 | 24 | func logIgnoredMessage( 25 | logger twelf.Logger, 26 | peerID ident.PeerID, 27 | msgID ident.MessageID, 28 | err error, 29 | ) { 30 | logger.Debug( 31 | "%s server ignored AMQP message %s, %s", 32 | peerID.ShortString(), 33 | msgID.ShortString(), 34 | err, 35 | ) 36 | } 37 | 38 | func logRequestBegin( 39 | ctx context.Context, 40 | logger twelf.Logger, 41 | peerID ident.PeerID, 42 | msgID ident.MessageID, 43 | req rinq.Request, 44 | ) { 45 | logger.Debug( 46 | "%s server began '%s::%s' command request %s [%s] <<< %s", 47 | peerID.ShortString(), 48 | req.Namespace, 49 | req.Command, 50 | msgID.ShortString(), 51 | trace.Get(ctx), 52 | req.Payload, 53 | ) 54 | } 55 | 56 | func logRequestEnd( 57 | ctx context.Context, 58 | logger twelf.Logger, 59 | peerID ident.PeerID, 60 | msgID ident.MessageID, 61 | req rinq.Request, 62 | payload *rinq.Payload, 63 | err error, 64 | ) { 65 | if !logger.IsDebug() { 66 | return 67 | } 68 | 69 | switch e := err.(type) { 70 | case nil: 71 | logger.Debug( 72 | "%s server completed '%s::%s' command request %s successfully [%s] >>> %s", 73 | peerID.ShortString(), 74 | req.Namespace, 75 | req.Command, 76 | msgID.ShortString(), 77 | trace.Get(ctx), 78 | payload, 79 | ) 80 | case rinq.Failure: 81 | var message string 82 | if e.Message != "" { 83 | message = ": " + e.Message 84 | } 85 | 86 | logger.Debug( 87 | "%s server completed '%s::%s' command request %s with '%s' failure%s [%s] <<< %s", 88 | peerID.ShortString(), 89 | req.Namespace, 90 | req.Command, 91 | msgID.ShortString(), 92 | e.Type, 93 | message, 94 | trace.Get(ctx), 95 | payload, 96 | ) 97 | default: 98 | logger.Debug( 99 | "%s server completed '%s::%s' command request %s with error [%s] <<< %s", 100 | peerID.ShortString(), 101 | req.Namespace, 102 | req.Command, 103 | msgID.ShortString(), 104 | trace.Get(ctx), 105 | err, 106 | ) 107 | } 108 | } 109 | 110 | func logNoLongerListening( 111 | logger twelf.Logger, 112 | peerID ident.PeerID, 113 | msgID ident.MessageID, 114 | ns string, 115 | ) { 116 | logger.Debug( 117 | "%s is no longer listening to '%s' namespace, request %s has been re-queued", 118 | peerID.ShortString(), 119 | ns, 120 | msgID.ShortString(), 121 | ) 122 | } 123 | 124 | func logRequestRequeued( 125 | ctx context.Context, 126 | logger twelf.Logger, 127 | peerID ident.PeerID, 128 | msgID ident.MessageID, 129 | req rinq.Request, 130 | ) { 131 | logger.Debug( 132 | "%s did not write a response for '%s::%s' command request, request %s has been re-queued [%s]", 133 | peerID.ShortString(), 134 | req.Namespace, 135 | req.Command, 136 | msgID.ShortString(), 137 | trace.Get(ctx), 138 | ) 139 | } 140 | 141 | func logRequestRejected( 142 | ctx context.Context, 143 | logger twelf.Logger, 144 | peerID ident.PeerID, 145 | msgID ident.MessageID, 146 | req rinq.Request, 147 | reason string, 148 | ) { 149 | logger.Log( 150 | "%s did not write a response for '%s::%s' command request %s, request has been abandoned (%s) [%s]", 151 | peerID.ShortString(), 152 | req.Namespace, 153 | req.Command, 154 | msgID.ShortString(), 155 | reason, 156 | trace.Get(ctx), 157 | ) 158 | } 159 | 160 | func logServerStart( 161 | logger twelf.Logger, 162 | peerID ident.PeerID, 163 | preFetch uint, 164 | ) { 165 | logger.Debug( 166 | "%s server started with (pre-fetch: %d)", 167 | peerID.ShortString(), 168 | preFetch, 169 | ) 170 | } 171 | 172 | func logServerStopping( 173 | logger twelf.Logger, 174 | peerID ident.PeerID, 175 | pending uint, 176 | ) { 177 | logger.Debug( 178 | "%s server is stopping gracefully (pending: %d)", 179 | peerID.ShortString(), 180 | pending, 181 | ) 182 | } 183 | 184 | func logServerStop( 185 | logger twelf.Logger, 186 | peerID ident.PeerID, 187 | err error, 188 | ) { 189 | if err == nil { 190 | logger.Debug( 191 | "%s server stopped", 192 | peerID.ShortString(), 193 | ) 194 | } else { 195 | logger.Debug( 196 | "%s server stopped: %s", 197 | peerID.ShortString(), 198 | err, 199 | ) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/notifyamqp/exchanges.go: -------------------------------------------------------------------------------- 1 | package notifyamqp 2 | 3 | import "github.com/streadway/amqp" 4 | 5 | const ( 6 | // unicastExchange is the exchange used to publish notifications directly to 7 | // a specific session. 8 | unicastExchange = "ntf.uc" 9 | 10 | // multicastExchange is the exchange used to publish notifications that are 11 | // sent to multiple sessions based on a rinq.Constraint. 12 | multicastExchange = "ntf.mc" 13 | ) 14 | 15 | func declareExchanges(channel *amqp.Channel) error { 16 | if err := channel.ExchangeDeclare( 17 | unicastExchange, 18 | "direct", 19 | false, // durable 20 | false, // autoDelete 21 | false, // internal 22 | false, // noWait 23 | nil, // args 24 | ); err != nil { 25 | return err 26 | } 27 | 28 | if err := channel.ExchangeDeclare( 29 | multicastExchange, 30 | "direct", 31 | false, // durable 32 | false, // autoDelete 33 | false, // internal 34 | false, // noWait 35 | nil, // args 36 | ); err != nil { 37 | return err 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/notifyamqp/factory.go: -------------------------------------------------------------------------------- 1 | package notifyamqp 2 | 3 | import ( 4 | "github.com/rinq/rinq-go/src/internal/localsession" 5 | "github.com/rinq/rinq-go/src/internal/notify" 6 | "github.com/rinq/rinq-go/src/internal/revisions" 7 | "github.com/rinq/rinq-go/src/rinq/ident" 8 | "github.com/rinq/rinq-go/src/rinq/options" 9 | "github.com/rinq/rinq-go/src/rinqamqp/internal/amqputil" 10 | ) 11 | 12 | // New returns a pair of notifier and listener. 13 | func New( 14 | peerID ident.PeerID, 15 | opts options.Options, 16 | sessions *localsession.Store, 17 | revs revisions.Store, 18 | channels amqputil.ChannelPool, 19 | ) (notify.Notifier, notify.Listener, error) { 20 | channel, err := channels.GetQOS(opts.SessionWorkers) // do not return to pool, use for listener 21 | if err != nil { 22 | return nil, nil, err 23 | } 24 | 25 | if err = declareExchanges(channel); err != nil { 26 | return nil, nil, err 27 | } 28 | 29 | listener, err := newListener( 30 | peerID, 31 | opts.SessionWorkers, 32 | sessions, 33 | revs, 34 | channel, 35 | opts.Logger, 36 | opts.Tracer, 37 | ) 38 | if err != nil { 39 | return nil, nil, err 40 | } 41 | 42 | return newNotifier(peerID, channels, opts.Logger), listener, nil 43 | } 44 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/notifyamqp/listener_logging.go: -------------------------------------------------------------------------------- 1 | package notifyamqp 2 | 3 | import ( 4 | "github.com/jmalloc/twelf/src/twelf" 5 | "github.com/rinq/rinq-go/src/rinq/ident" 6 | ) 7 | 8 | func logInvalidMessageID( 9 | logger twelf.Logger, 10 | peerID ident.PeerID, 11 | msgID string, 12 | ) { 13 | logger.Debug( 14 | "%s listener ignored AMQP message, '%s' is not a valid message ID", 15 | peerID.ShortString(), 16 | msgID, 17 | ) 18 | } 19 | 20 | func logIgnoredMessage( 21 | logger twelf.Logger, 22 | peerID ident.PeerID, 23 | msgID ident.MessageID, 24 | err error, 25 | ) { 26 | logger.Debug( 27 | "%s listener ignored AMQP message %s, %s", 28 | peerID.ShortString(), 29 | msgID.ShortString(), 30 | err, 31 | ) 32 | } 33 | 34 | func logListenerStart( 35 | logger twelf.Logger, 36 | peerID ident.PeerID, 37 | preFetch uint, 38 | ) { 39 | logger.Debug( 40 | "%s listener started (pre-fetch: %d)", 41 | peerID.ShortString(), 42 | preFetch, 43 | ) 44 | } 45 | 46 | func logListenerStopping( 47 | logger twelf.Logger, 48 | peerID ident.PeerID, 49 | pending uint, 50 | ) { 51 | logger.Debug( 52 | "%s listener stopping gracefully (pending: %d)", 53 | peerID.ShortString(), 54 | pending, 55 | ) 56 | } 57 | 58 | func logListenerStop( 59 | logger twelf.Logger, 60 | peerID ident.PeerID, 61 | err error, 62 | ) { 63 | if err == nil { 64 | logger.Debug( 65 | "%s listener stopped", 66 | peerID.ShortString(), 67 | ) 68 | } else { 69 | logger.Debug( 70 | "%s listener stopped: %s", 71 | peerID.ShortString(), 72 | err, 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/notifyamqp/message.go: -------------------------------------------------------------------------------- 1 | package notifyamqp 2 | 3 | import ( 4 | "errors" 5 | 6 | opentracing "github.com/opentracing/opentracing-go" 7 | "github.com/opentracing/opentracing-go/ext" 8 | "github.com/rinq/rinq-go/src/internal/opentr" 9 | "github.com/rinq/rinq-go/src/internal/x/bufferpool" 10 | "github.com/rinq/rinq-go/src/internal/x/cbor" 11 | "github.com/rinq/rinq-go/src/rinq" 12 | "github.com/rinq/rinq-go/src/rinq/constraint" 13 | "github.com/rinq/rinq-go/src/rinq/ident" 14 | "github.com/rinq/rinq-go/src/rinqamqp/internal/amqputil" 15 | "github.com/streadway/amqp" 16 | ) 17 | 18 | const ( 19 | // namespaceHeader specifies the namespace in notifications 20 | namespaceHeader = "n" 21 | 22 | // targetHeader specifies the target session for unicast notifications 23 | targetHeader = "t" 24 | 25 | // constraintHeader specifies the constraint for multicast notifications. 26 | constraintHeader = "c" 27 | ) 28 | 29 | func unicastRoutingKey(ns string, p ident.PeerID) string { 30 | return ns + "." + p.String() 31 | } 32 | 33 | func packCommonAttributes( 34 | msg *amqp.Publishing, 35 | traceID string, 36 | ns string, 37 | t string, 38 | p *rinq.Payload, 39 | ) { 40 | msg.Type = t 41 | msg.Body = p.Bytes() 42 | 43 | if msg.Headers == nil { 44 | msg.Headers = amqp.Table{} 45 | } 46 | 47 | msg.Headers[namespaceHeader] = ns 48 | 49 | amqputil.PackTrace(msg, traceID) 50 | } 51 | 52 | func unpackCommonAttributes(msg *amqp.Delivery) (ns, t string, p *rinq.Payload, err error) { 53 | t = msg.Type 54 | p = rinq.NewPayloadFromBytes(msg.Body) 55 | 56 | ns, ok := msg.Headers[namespaceHeader].(string) 57 | if !ok { 58 | err = errors.New("namespace header is not a string") 59 | } 60 | 61 | return 62 | } 63 | 64 | func packTarget(msg *amqp.Publishing, target ident.SessionID) { 65 | if msg.Headers == nil { 66 | msg.Headers = amqp.Table{} 67 | } 68 | 69 | msg.Headers[targetHeader] = target.String() 70 | } 71 | 72 | func unpackTarget(msg *amqp.Delivery) (id ident.SessionID, err error) { 73 | if t, ok := msg.Headers[targetHeader].(string); ok { 74 | id, err = ident.ParseSessionID(t) 75 | } else { 76 | err = errors.New("target header is not a string") 77 | } 78 | 79 | return 80 | } 81 | 82 | func packConstraint(msg *amqp.Publishing, con constraint.Constraint) { 83 | if msg.Headers == nil { 84 | msg.Headers = amqp.Table{} 85 | } 86 | 87 | // don't return buf to the pool as it's internal buffer is retained inside 88 | // the msg header. 89 | buf := bufferpool.Get() 90 | cbor.MustEncode(buf, con) 91 | 92 | msg.Headers[constraintHeader] = buf.Bytes() 93 | } 94 | 95 | func unpackConstraint(msg *amqp.Delivery) (con constraint.Constraint, err error) { 96 | if buf, ok := msg.Headers[constraintHeader].([]byte); ok { 97 | err = cbor.DecodeBytes(buf, &con) 98 | } else { 99 | err = errors.New("constraint header is not a byte slice") 100 | } 101 | 102 | return 103 | } 104 | 105 | func unpackSpanOptions(msg *amqp.Delivery, t opentracing.Tracer) (opts []opentracing.StartSpanOption, err error) { 106 | sc, err := amqputil.UnpackSpanContext(msg, t) 107 | 108 | if err == nil { 109 | opts = append(opts, opentr.CommonSpanOptions...) 110 | opts = append(opts, ext.SpanKindConsumer) 111 | 112 | if sc != nil { 113 | opts = append(opts, opentracing.FollowsFrom(sc)) 114 | } 115 | } 116 | 117 | return 118 | } 119 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/notifyamqp/notifier.go: -------------------------------------------------------------------------------- 1 | package notifyamqp 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmalloc/twelf/src/twelf" 7 | "github.com/rinq/rinq-go/src/internal/notify" 8 | "github.com/rinq/rinq-go/src/internal/service" 9 | "github.com/rinq/rinq-go/src/rinq" 10 | "github.com/rinq/rinq-go/src/rinq/constraint" 11 | "github.com/rinq/rinq-go/src/rinq/ident" 12 | "github.com/rinq/rinq-go/src/rinqamqp/internal/amqputil" 13 | "github.com/streadway/amqp" 14 | ) 15 | 16 | type notifier struct { 17 | service.Service 18 | sm *service.StateMachine 19 | 20 | peerID ident.PeerID 21 | channels amqputil.ChannelPool 22 | logger twelf.Logger 23 | } 24 | 25 | // newNotifier creates, initializes and returns a new notifier. 26 | func newNotifier( 27 | peerID ident.PeerID, 28 | channels amqputil.ChannelPool, 29 | logger twelf.Logger, 30 | ) notify.Notifier { 31 | n := ¬ifier{ 32 | peerID: peerID, 33 | channels: channels, 34 | logger: logger, 35 | } 36 | 37 | n.sm = service.NewStateMachine(n.run, n.finalize) 38 | n.Service = n.sm 39 | 40 | go n.sm.Run() 41 | 42 | return n 43 | } 44 | 45 | func (n *notifier) NotifyUnicast( 46 | ctx context.Context, 47 | msgID ident.MessageID, 48 | traceID string, 49 | target ident.SessionID, 50 | ns string, 51 | notificationType string, 52 | payload *rinq.Payload, 53 | ) (err error) { 54 | msg := amqp.Publishing{ 55 | MessageId: msgID.String(), 56 | } 57 | 58 | packCommonAttributes(&msg, traceID, ns, notificationType, payload) 59 | packTarget(&msg, target) 60 | 61 | err = amqputil.PackSpanContext(ctx, &msg) 62 | 63 | if err == nil { 64 | err = n.send(unicastExchange, unicastRoutingKey(ns, target.Peer), msg) 65 | } 66 | 67 | return 68 | } 69 | 70 | func (n *notifier) NotifyMulticast( 71 | ctx context.Context, 72 | msgID ident.MessageID, 73 | traceID string, 74 | con constraint.Constraint, 75 | ns string, 76 | notificationType string, 77 | payload *rinq.Payload, 78 | ) (err error) { 79 | msg := amqp.Publishing{ 80 | MessageId: msgID.String(), 81 | } 82 | 83 | packCommonAttributes(&msg, traceID, ns, notificationType, payload) 84 | packConstraint(&msg, con) 85 | 86 | err = amqputil.PackSpanContext(ctx, &msg) 87 | 88 | if err == nil { 89 | err = n.send(multicastExchange, ns, msg) 90 | } 91 | 92 | return 93 | } 94 | 95 | func (n *notifier) send(exchange, key string, msg amqp.Publishing) error { 96 | select { 97 | case <-n.sm.Graceful: 98 | return context.Canceled 99 | case <-n.sm.Forceful: 100 | return context.Canceled 101 | default: 102 | // ready to publish 103 | } 104 | 105 | channel, err := n.channels.Get() 106 | if err != nil { 107 | return err 108 | } 109 | defer n.channels.Put(channel) 110 | 111 | return channel.Publish( 112 | exchange, 113 | key, 114 | false, // mandatory 115 | false, // immediate 116 | msg, 117 | ) 118 | } 119 | 120 | func (n *notifier) run() (service.State, error) { 121 | logNotifierStart(n.logger, n.peerID) 122 | 123 | select { 124 | case <-n.sm.Graceful: 125 | return nil, nil 126 | 127 | case <-n.sm.Forceful: 128 | return nil, nil 129 | } 130 | } 131 | 132 | func (n *notifier) finalize(err error) error { 133 | logNotifierStop(n.logger, n.peerID, err) 134 | return err 135 | } 136 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/notifyamqp/notifier_logging.go: -------------------------------------------------------------------------------- 1 | package notifyamqp 2 | 3 | import ( 4 | "github.com/jmalloc/twelf/src/twelf" 5 | "github.com/rinq/rinq-go/src/rinq/ident" 6 | ) 7 | 8 | func logNotifierStart( 9 | logger twelf.Logger, 10 | peerID ident.PeerID, 11 | ) { 12 | logger.Debug( 13 | "%s notifier started", 14 | peerID.ShortString(), 15 | ) 16 | } 17 | 18 | func logNotifierStop( 19 | logger twelf.Logger, 20 | peerID ident.PeerID, 21 | err error, 22 | ) { 23 | if err == nil { 24 | logger.Debug( 25 | "%s notifier stopped", 26 | peerID.ShortString(), 27 | ) 28 | } else { 29 | logger.Debug( 30 | "%s notifier stopped: %s", 31 | peerID.ShortString(), 32 | err, 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/rinqamqp/internal/notifyamqp/queues.go: -------------------------------------------------------------------------------- 1 | package notifyamqp 2 | 3 | import "github.com/rinq/rinq-go/src/rinq/ident" 4 | 5 | // notifyQueue returns the name of the queue used for incoming notifications. 6 | func notifyQueue(id ident.PeerID) string { 7 | return id.ShortString() + ".ntf" 8 | } 9 | -------------------------------------------------------------------------------- /src/rinqamqp/peer_logging.go: -------------------------------------------------------------------------------- 1 | package rinqamqp 2 | 3 | import ( 4 | "github.com/jmalloc/twelf/src/twelf" 5 | "github.com/rinq/rinq-go/src/rinq/ident" 6 | ) 7 | 8 | func logStartedListening( 9 | logger twelf.Logger, 10 | peerID ident.PeerID, 11 | namespace string, 12 | ) { 13 | logger.Log( 14 | "%s started listening for command requests in '%s' namespace", 15 | peerID.ShortString(), 16 | namespace, 17 | ) 18 | } 19 | 20 | func logStoppedListening( 21 | logger twelf.Logger, 22 | peerID ident.PeerID, 23 | namespace string, 24 | ) { 25 | logger.Log( 26 | "%s stopped listening for command requests in '%s' namespace", 27 | peerID.ShortString(), 28 | namespace, 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/rinqamqp/pkg.go: -------------------------------------------------------------------------------- 1 | // Package rinqamqp provides an AMQP-based Rinq implementation. 2 | package rinqamqp 3 | --------------------------------------------------------------------------------