├── .gitignore ├── Gopkg.toml ├── LICENSE ├── README.md ├── docker └── docker-compose.yml └── gosiris ├── actor.go ├── actor_options.go ├── actor_ref.go ├── actor_test.go ├── dispatcher.go ├── etcd.go ├── log.go ├── registry.go ├── system.go ├── transport.go ├── transport_amqp.go ├── transport_kafka.go ├── zipkin.go └── zipkin_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | /vendor/* 8 | Gopkg.lock 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 17 | .glide/ 18 | 19 | # Binaries for programs and plugins 20 | *.exe 21 | *.dll 22 | *.so 23 | *.dylib 24 | 25 | # Test binary, build with `go test -c` 26 | *.test 27 | 28 | # Output of the go coverage tool, specifically when used with LiteIDE 29 | *.out 30 | 31 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 32 | .glide/ 33 | 34 | .idea/.name 35 | .idea/golang-design-patterns.iml 36 | .idea/modules.xml 37 | .idea/saveactions_settings.xml 38 | .idea/vcs.xml 39 | .idea/gosiris.iml 40 | 41 | # User-specific stuff: 42 | .idea/**/workspace.xml 43 | .idea/**/tasks.xml 44 | .idea/dictionaries 45 | 46 | # Sensitive or high-churn files: 47 | .idea/**/dataSources/ 48 | .idea/**/dataSources.ids 49 | .idea/**/dataSources.xml 50 | .idea/**/dataSources.local.xml 51 | .idea/**/sqlDataSources.xml 52 | .idea/**/dynamic.xml 53 | .idea/**/uiDesigner.xml 54 | 55 | # Gradle: 56 | .idea/**/gradle.xml 57 | .idea/**/libraries 58 | 59 | # CMake 60 | cmake-build-debug/ 61 | 62 | # Mongo Explorer plugin: 63 | .idea/**/mongoSettings.xml 64 | 65 | ## File-based project format: 66 | *.iws 67 | 68 | ## Plugin-specific files: 69 | 70 | # IntelliJ 71 | out/ 72 | 73 | # mpeltonen/sbt-idea plugin 74 | .idea_modules/ 75 | 76 | # JIRA plugin 77 | atlassian-ide-plugin.xml 78 | 79 | # Cursive Clojure plugin 80 | .idea/replstate.xml 81 | 82 | # Crashlytics plugin (for Android Studio and IntelliJ) 83 | com_crashlytics_export_strings.xml 84 | crashlytics.properties 85 | crashlytics-build.properties 86 | fabric.properties 87 | 88 | pkg/ 89 | pkg/** 90 | 91 | /notes 92 | _config.yml 93 | misc.xml -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | [[constraint]] 2 | name = "github.com/coreos/etcd" 3 | version = "3.2.9" 4 | 5 | [[constraint]] 6 | branch = "master" 7 | name = "github.com/streadway/amqp" 8 | 9 | [[constraint]] 10 | name = "github.com/openzipkin/zipkin-go-opentracing" 11 | version = "0.2.4" 12 | 13 | [[constraint]] 14 | name = "github.com/apache/thrift" 15 | version = "0.10.0" 16 | 17 | [[constraint]] 18 | name = "github.com/Shopify/sarama" 19 | version = "1.13.0" 20 | 21 | [[constraint]] 22 | name = "github.com/opentracing/opentracing-go" 23 | version = "1.0.2" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/gojp/goreportcard)](https://goreportcard.com/report/gojp/goreportcard) 2 | 3 | gosiris is an [actor](https://en.wikipedia.org/wiki/Actor_model) framework for Golang. 4 | 5 | # Features 6 | 7 | * Manage a hierarchy of actors (each actor has its own: state, behavior, mailbox, child actors) 8 | * Deploy remote actors accessible though an AMQP broker or Kafka 9 | * Automated registration and runtime discoverability using etcd registry 10 | * Zipkin integration 11 | * Built-in patterns (become/unbecome, send, forward, repeat, child supervision) 12 | 13 | # Examples 14 | 15 | ## Hello world 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "gosiris/gosiris" 22 | ) 23 | 24 | func main() { 25 | //Init a local actor system 26 | gosiris.InitActorSystem(gosiris.SystemOptions{ 27 | ActorSystemName: "ActorSystem", 28 | }) 29 | 30 | //Create an actor 31 | parentActor := gosiris.Actor{} 32 | //Close an actor 33 | defer parentActor.Close() 34 | 35 | //Create an actor 36 | childActor := gosiris.Actor{} 37 | //Close an actor 38 | defer childActor.Close() 39 | //Register a reaction to event types ("message" in this case) 40 | childActor.React("message", func(context gosiris.Context) { 41 | context.Self.LogInfo(context, "Received %v\n", context.Data) 42 | }) 43 | 44 | //Register an actor to the system 45 | gosiris.ActorSystem().RegisterActor("parentActor", &parentActor, nil) 46 | //Register an actor by spawning it 47 | gosiris.ActorSystem().SpawnActor(&parentActor, "childActor", &childActor, nil) 48 | 49 | //Retrieve actor references 50 | parentActorRef, _ := gosiris.ActorSystem().ActorOf("parentActor") 51 | childActorRef, _ := gosiris.ActorSystem().ActorOf("childActor") 52 | 53 | //Send a message from one actor to another (from parentActor to childActor) 54 | childActorRef.Tell(gosiris.EmptyContext, "message", "Hi! How are you?", parentActorRef) 55 | } 56 | ``` 57 | 58 | ``` 59 | INFO: [childActor] 1988/01/08 01:00:00 Received Hi! How are you? 60 | ``` 61 | 62 | ## Distributed actor system example 63 | 64 | In the following example, **in less than 30 effective lines of code**, we will see how to create a distributed actor system implementing a request/reply interaction. An actor will be triggered by AMQP messages while another one will be triggered by Kafka events. Each actor will register itself in an etcd instance and will discover the other actor at runtime. Last but not least, gosiris will also manage the Zipkin integration by automatically managing the spans and forwarding the logs. 65 | 66 | ```go 67 | package main 68 | 69 | import ( 70 | "gosiris/gosiris" 71 | "time" 72 | ) 73 | 74 | func main() { 75 | //Configure a distributed actor system with an etcd registry and a Zipkin integration 76 | gosiris.InitActorSystem(gosiris.SystemOptions{ 77 | ActorSystemName: "ActorSystem", 78 | RegistryUrl: "http://etcd:2379", 79 | ZipkinOptions: gosiris.ZipkinOptions{ 80 | Url: "http://zipkin:9411/api/v1/spans", 81 | Debug: true, 82 | HostPort: "0.0.0.0", 83 | SameSpan: true, 84 | }, 85 | }) 86 | //Defer the actor system closure 87 | defer gosiris.CloseActorSystem() 88 | 89 | //Configure actor1 90 | actor1 := new(gosiris.Actor).React("reply", func(context gosiris.Context) { 91 | //Because Zipkin is enabled, the log will be also sent to the Zipkin server 92 | context.Self.LogInfo(context, "Received: %v", context.Data) 93 | 94 | }) 95 | //Defer actor1 closure 96 | defer actor1.Close() 97 | //Register a remote actor accessible through AMQP 98 | gosiris.ActorSystem().RegisterActor("actor1", actor1, new(gosiris.ActorOptions).SetRemote(true).SetRemoteType(gosiris.Amqp).SetUrl("amqp://guest:guest@amqp:5672/").SetDestination("actor1")) 99 | 100 | //Configure actor2 101 | actor2 := new(gosiris.Actor).React("context", func(context gosiris.Context) { 102 | //Because Zipkin is enabled, the log will be also sent to the Zipkin server 103 | context.Self.LogInfo(context, "Received: %v", context.Data) 104 | context.Sender.Tell(context, "reply", "hello back", context.Self) 105 | }) 106 | //Defer actor2 closure 107 | defer actor2.Close() 108 | //Register a remote actor accessible through Kafka 109 | gosiris.ActorSystem().SpawnActor(actor1, "actor2", actor2, new(gosiris.ActorOptions).SetRemote(true).SetRemoteType(gosiris.Kafka).SetUrl("kafka:9092").SetDestination("actor2")) 110 | 111 | //Retrieve the actor references 112 | actor1Ref, _ := gosiris.ActorSystem().ActorOf("actor1") 113 | actor2Ref, _ := gosiris.ActorSystem().ActorOf("actor2") 114 | 115 | //Send a message to the kafkaRef 116 | actor2Ref.Tell(gosiris.EmptyContext, "context", "hello", actor1Ref) 117 | 118 | time.Sleep(250 * time.Millisecond) 119 | } 120 | 121 | ``` 122 | 123 | ``` 124 | INFO: [actor2] 2017/11/11 00:38:24 Received: hello 125 | INFO: [actor1] 2017/11/11 00:38:24 Received: hello back 126 | ``` 127 | 128 | ## More Examples 129 | 130 | See the examples in [actor_test.go](gosiris/actor_test.go). 131 | 132 | # Environment 133 | 134 | First of all, to run the examples you must configure the following hostnames: _etcd_, _amqp_, _zipkin_, and _kafka_. 135 | Then to setup the full environment, you can simply run the [Docker Compose](docker/docker-compose.yml). 136 | 137 | # Troubleshooting 138 | 139 | You may experience errors during the tests like the following: 140 | ``` 141 | r.EncodeArrayStart undefined (type codec.encDriver has no field or method EncodeArrayStart) 142 | ``` 143 | 144 | This is a known issue with the etcd client used. The manual workaround (for the time being) is to delete manually the file _keys.generated.go_ generated in /vendor. 145 | 146 | # Contributing 147 | 148 | * Open an issue if you want a new feature or if you spotted a bug 149 | * Feel free to propose pull requests 150 | 151 | Any contribution is more than welcome! In the meantime, if we want to discuss gosiris you can contact me [@teivah](https://twitter.com/teivah). -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | etcd: 4 | image: "quay.io/coreos/etcd:v2.3.8" 5 | ports: 6 | - "4001:4001" 7 | - "2380:2380" 8 | - "2379:2379" 9 | command: 10 | -name etcd0 11 | -advertise-client-urls "http://etcd:2379,http://etcd:4001" 12 | -listen-client-urls "http://0.0.0.0:2379,http://0.0.0.0:4001" 13 | -initial-advertise-peer-urls http://${HostIP}:2380 14 | -listen-peer-urls http://0.0.0.0:2380 15 | -initial-cluster-token etcd-cluster-1 16 | -initial-cluster "etcd0=http://${HostIP}:2380" 17 | -initial-cluster-state new 18 | 19 | rabbit: 20 | image: "rabbitmq" 21 | ports: 22 | - "4369:4369" 23 | - "5671:5671" 24 | - "5672:5672" 25 | - "15672:15672" 26 | 27 | kafka: 28 | environment: 29 | - ADVERTISED_HOST=kafka 30 | - ADVERTISED_PORT=9092 31 | image: "teivah/kafka" 32 | ports: 33 | - "2181:2181" 34 | - "9092:9092" 35 | 36 | zipkin: 37 | image: openzipkin/zipkin 38 | ports: 39 | - "9411:9411" -------------------------------------------------------------------------------- /gosiris/actor.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import "github.com/opentracing/opentracing-go" 4 | 5 | func RootActor() *Actor { 6 | return root 7 | } 8 | 9 | type Actor struct { 10 | name string 11 | conf map[string]func(Context) 12 | dataChan chan Context 13 | closeChan chan interface{} 14 | parent actorInterface 15 | unbecome map[string]func(Context) 16 | span opentracing.Span 17 | } 18 | 19 | type RemoteActor struct { 20 | Actor 21 | } 22 | 23 | type actorInterface interface { 24 | React(string, func(Context)) *Actor 25 | reactions() map[string]func(Context) 26 | unbecomeHistory() map[string]func(Context) 27 | getDataChan() chan Context 28 | setDataChan(chan Context) 29 | getCloseChan() chan interface{} 30 | setCloseChan(chan interface{}) 31 | setName(string) 32 | setParent(actorInterface) 33 | Parent() ActorRefInterface 34 | Name() string 35 | Close() 36 | } 37 | 38 | func (actor *Actor) Close() { 39 | ActorSystem().closeLocalActor(actor.name) 40 | } 41 | 42 | func (actor *Actor) String() string { 43 | return actor.name 44 | } 45 | 46 | func (actor *Actor) React(messageType string, f func(Context)) *Actor { 47 | if actor.conf == nil { 48 | actor.conf = make(map[string]func(Context)) 49 | actor.unbecome = make(map[string]func(Context)) 50 | } 51 | 52 | actor.conf[messageType] = f 53 | 54 | return actor 55 | } 56 | 57 | func (actor *Actor) reactions() map[string]func(Context) { 58 | return actor.conf 59 | } 60 | 61 | func (actor *Actor) unbecomeHistory() map[string]func(Context) { 62 | return actor.unbecome 63 | } 64 | 65 | func (actor *Actor) getDataChan() chan Context { 66 | return actor.dataChan 67 | } 68 | 69 | func (actor *Actor) setDataChan(dataChan chan Context) { 70 | actor.dataChan = dataChan 71 | } 72 | 73 | func (actor *Actor) getCloseChan() chan interface{} { 74 | return actor.closeChan 75 | } 76 | 77 | func (actor *Actor) setCloseChan(closeChan chan interface{}) { 78 | actor.closeChan = closeChan 79 | } 80 | 81 | func (actor *Actor) setName(name string) { 82 | actor.name = name 83 | } 84 | 85 | func (actor *Actor) setParent(parent actorInterface) { 86 | actor.parent = parent 87 | } 88 | 89 | func (actor *Actor) Parent() ActorRefInterface { 90 | parent, _ := ActorSystem().ActorOf(actor.parent.Name()) 91 | return parent 92 | } 93 | 94 | func (actor *Actor) Name() string { 95 | return actor.name 96 | } 97 | -------------------------------------------------------------------------------- /gosiris/actor_options.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import "time" 4 | 5 | const ( 6 | defaultBufferSize = 64 7 | ) 8 | 9 | type ActorOptions struct { 10 | parent string 11 | remote bool //Default: false 12 | autoclose bool //Default: false 13 | remoteType string 14 | url string 15 | destination string 16 | bufferSize int //Default: 64 17 | defaultWatcher time.Duration 18 | } 19 | 20 | //TODO No interface 21 | type OptionsInterface interface { 22 | SetUrl(string) OptionsInterface 23 | SetRemote(bool) OptionsInterface 24 | SetAutoclose(bool) OptionsInterface 25 | Remote() bool 26 | Autoclose() bool 27 | SetRemoteType(string) OptionsInterface 28 | RemoteType() string 29 | Url() string 30 | SetDestination(string) OptionsInterface 31 | Destination() string 32 | setParent(string) 33 | Parent() string 34 | SetBufferSize(int) OptionsInterface 35 | BufferSize() int 36 | SetDefaultWatcher(time.Duration) OptionsInterface 37 | DefaultWatcher() time.Duration 38 | } 39 | 40 | func (options *ActorOptions) SetRemote(b bool) OptionsInterface { 41 | options.remote = b 42 | return options 43 | } 44 | 45 | func (options *ActorOptions) Remote() bool { 46 | return options.remote 47 | } 48 | 49 | func (options *ActorOptions) SetAutoclose(b bool) OptionsInterface { 50 | options.autoclose = b 51 | return options 52 | } 53 | 54 | func (options *ActorOptions) Autoclose() bool { 55 | return options.autoclose 56 | } 57 | 58 | func (options *ActorOptions) SetRemoteType(s string) OptionsInterface { 59 | options.remoteType = s 60 | return options 61 | } 62 | 63 | func (options *ActorOptions) RemoteType() string { 64 | return options.remoteType 65 | } 66 | 67 | func (options *ActorOptions) SetUrl(s string) OptionsInterface { 68 | options.url = s 69 | return options 70 | } 71 | 72 | func (options *ActorOptions) Url() string { 73 | return options.url 74 | } 75 | 76 | func (options *ActorOptions) SetDestination(s string) OptionsInterface { 77 | options.destination = s 78 | return options 79 | } 80 | 81 | func (options *ActorOptions) Destination() string { 82 | return options.destination 83 | } 84 | 85 | func (options *ActorOptions) setParent(s string) { 86 | options.parent = s 87 | } 88 | 89 | func (options *ActorOptions) Parent() string { 90 | return options.parent 91 | } 92 | 93 | func (options *ActorOptions) SetBufferSize(i int) OptionsInterface { 94 | options.bufferSize = i 95 | return options 96 | } 97 | 98 | func (options *ActorOptions) BufferSize() int { 99 | return options.bufferSize 100 | } 101 | 102 | func (options *ActorOptions) SetDefaultWatcher(d time.Duration) OptionsInterface { 103 | options.defaultWatcher = d 104 | return options 105 | } 106 | 107 | func (options *ActorOptions) DefaultWatcher() time.Duration { 108 | return options.defaultWatcher 109 | } 110 | -------------------------------------------------------------------------------- /gosiris/actor_ref.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import ( 4 | "fmt" 5 | "github.com/opentracing/opentracing-go" 6 | "log" 7 | "time" 8 | ) 9 | 10 | type ActorRef struct { 11 | name string 12 | infoLogger *log.Logger 13 | errorLogger *log.Logger 14 | } 15 | 16 | type ActorRefInterface interface { 17 | Tell(Context, string, interface{}, ActorRefInterface) error 18 | Repeat(string, time.Duration, interface{}, ActorRefInterface) (chan struct{}, error) 19 | AskForClose(ActorRefInterface) 20 | LogInfo(Context, string, ...interface{}) 21 | LogError(Context, string, ...interface{}) 22 | Become(string, func(Context)) error 23 | Unbecome(string) error 24 | Name() string 25 | Forward(Context, ...string) 26 | } 27 | 28 | func newActorRef(name string) ActorRefInterface { 29 | ref := ActorRef{} 30 | ref.infoLogger, ref.errorLogger = 31 | NewActorLogger(name) 32 | ref.name = name 33 | return ref 34 | } 35 | 36 | func (ref ActorRef) LogInfo(context Context, format string, a ...interface{}) { 37 | ref.infoLogger.Printf(format, a...) 38 | if zipkinSystemInitialized { 39 | logZipkinMessage(context.span, fmt.Sprintf(format, a...)) 40 | } 41 | } 42 | 43 | func (ref ActorRef) LogError(context Context, format string, a ...interface{}) { 44 | ref.errorLogger.Printf(format, a...) 45 | if zipkinSystemInitialized { 46 | logZipkinMessage(context.span, fmt.Sprintf(format, a...)) 47 | } 48 | } 49 | 50 | func (ref ActorRef) Tell(context Context, messageType string, data interface{}, sender ActorRefInterface) error { 51 | actor, err := ActorSystem().actor(ref.name) 52 | 53 | if err != nil { 54 | ErrorLogger.Printf("Failed to send from %v to %v: %v", sender.Name(), ref.name, err) 55 | return err 56 | } 57 | 58 | var span opentracing.Span = nil 59 | if context.span != nil { 60 | span = context.span 61 | } else if zipkinSystemInitialized && messageType != GosirisMsgChildClosed { 62 | span = startZipkinSpan(sender.Name(), messageType) 63 | } 64 | 65 | dispatch(actor.actor.getDataChan(), messageType, data, &ref, sender, actor.options, span) 66 | 67 | if span != nil { 68 | stopZipkinSpan(span) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (ref ActorRef) Repeat(messageType string, d time.Duration, data interface{}, sender ActorRefInterface) (chan struct{}, error) { 75 | actor, err := ActorSystem().actor(ref.name) 76 | 77 | if err != nil { 78 | ErrorLogger.Printf("Failed to send from %v to %v: %v", sender.Name(), ref.name, err) 79 | return nil, err 80 | } 81 | 82 | t := time.NewTicker(d) 83 | stop := make(chan struct{}) 84 | 85 | go func(t *time.Ticker, stop chan struct{}) { 86 | for { 87 | select { 88 | case <-t.C: 89 | dispatch(actor.actor.getDataChan(), messageType, data, &ref, sender, actor.options, nil) 90 | case <-stop: 91 | t.Stop() 92 | close(stop) 93 | return 94 | } 95 | } 96 | }(t, stop) 97 | 98 | return stop, nil 99 | } 100 | 101 | func (ref ActorRef) AskForClose(sender ActorRefInterface) { 102 | InfoLogger.Printf("Asking to close %v", ref.name) 103 | 104 | actor, err := ActorSystem().actor(ref.name) 105 | 106 | if err != nil { 107 | InfoLogger.Printf("Actor %v already closed", ref.name) 108 | return 109 | } 110 | 111 | go dispatch(actor.actor.getDataChan(), GosirisMsgPoisonPill, nil, &ref, sender, actor.options, nil) 112 | } 113 | 114 | func (ref ActorRef) Become(messageType string, f func(Context)) error { 115 | actor, err := ActorSystem().actor(ref.name) 116 | if err != nil { 117 | return fmt.Errorf("actor implementation %v not found", messageType) 118 | } 119 | 120 | if actor.actor.reactions() == nil { 121 | return fmt.Errorf("react for %v not yet implemented", messageType) 122 | } 123 | 124 | v, exists := actor.actor.reactions()[messageType] 125 | 126 | if !exists { 127 | return fmt.Errorf("react for %v not yet implemented", messageType) 128 | } 129 | 130 | actor.actor.unbecomeHistory()[messageType] = v 131 | actor.actor.reactions()[messageType] = f 132 | 133 | return nil 134 | } 135 | 136 | func (ref ActorRef) Unbecome(messageType string) error { 137 | actor, err := ActorSystem().actor(ref.name) 138 | if err != nil { 139 | return fmt.Errorf("actor implementation %v not found", messageType) 140 | } 141 | 142 | if actor.actor.reactions() == nil { 143 | return fmt.Errorf("become for %v not yet implemented", messageType) 144 | } 145 | 146 | v, exists := actor.actor.unbecomeHistory()[messageType] 147 | 148 | if !exists { 149 | return fmt.Errorf("unbecome for %v not yet implemented", messageType) 150 | } 151 | 152 | actor.actor.reactions()[messageType] = v 153 | delete(actor.actor.unbecomeHistory(), messageType) 154 | 155 | return nil 156 | } 157 | 158 | func (ref ActorRef) Name() string { 159 | return ref.name 160 | } 161 | 162 | func (ref ActorRef) Forward(context Context, destinations ...string) { 163 | for _, v := range destinations { 164 | actorRef, err := ActorSystem().ActorOf(v) 165 | if err != nil { 166 | ErrorLogger.Printf("actor %v is not part of the actor system", v) 167 | } 168 | actorRef.Tell(context, context.MessageType, context.Data, context.Sender) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /gosiris/actor_test.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | type ParentActor struct { 10 | Actor 11 | } 12 | 13 | type ChildActor struct { 14 | Actor 15 | hello map[string]bool 16 | } 17 | 18 | func TestBasic(t *testing.T) { 19 | t.Log("Starting Basic test") 20 | 21 | //Init a local actor system 22 | InitActorSystem(SystemOptions{ 23 | ActorSystemName: "ActorSystem", 24 | }) 25 | //Defer the actor system closure 26 | defer CloseActorSystem() 27 | 28 | //Create a simple parent actor 29 | parentActor := Actor{} 30 | //Defer the actor closure 31 | defer parentActor.Close() 32 | 33 | //Register the parent actor 34 | ActorSystem().RegisterActor("parentActor", &parentActor, nil) 35 | 36 | //Create a simple child actor 37 | childActor := Actor{} 38 | //Defer the actor system closure 39 | defer childActor.Close() 40 | 41 | //Register the reactions to event types (here a reaction to context) 42 | childActor.React("context", func(context Context) { 43 | context.Self.LogInfo(context, "Received %v\n", context.Data) 44 | }) 45 | 46 | //Register the child actor 47 | ActorSystem().SpawnActor(&parentActor, "childActor", &childActor, nil) 48 | 49 | //Retrieve the parent and child actor reference 50 | parentActorRef, _ := ActorSystem().ActorOf("parentActor") 51 | childActorRef, _ := ActorSystem().ActorOf("childActor") 52 | 53 | //Tell a context from the parent to the child actor 54 | childActorRef.Tell(EmptyContext, "context", "Hi! How are you?", parentActorRef) 55 | 56 | time.Sleep(2500 * time.Millisecond) 57 | } 58 | 59 | func TestStatefulness(t *testing.T) { 60 | t.Log("Starting statefulness test") 61 | 62 | opts := SystemOptions{ 63 | ActorSystemName: "ActorSystem", 64 | } 65 | InitActorSystem(opts) 66 | defer CloseActorSystem() 67 | 68 | childActor := ChildActor{} 69 | childActor.hello = make(map[string]bool) 70 | 71 | parentActor := ParentActor{} 72 | defer parentActor.Close() 73 | 74 | f := func(context Context) { 75 | context.Self.LogInfo(context, "Receive response %v\n", context.Data) 76 | } 77 | 78 | parentActor.React("helloback", f).React("error", f).React("help", f) 79 | ActorSystem().RegisterActor("parent", &parentActor, nil) 80 | 81 | childActor.React("hello", func(context Context) { 82 | context.Self.LogInfo(context, "Receive request %v\n", context.Data) 83 | 84 | name := context.Data.(string) 85 | 86 | if _, ok := childActor.hello[name]; ok { 87 | context.Sender.Tell(context, "error", "I already know you!", context.Self) 88 | childActor.Parent().Tell(context, "help", "Daddy help me!", context.Self) 89 | childActor.Close() 90 | } else { 91 | childActor.hello[context.Data.(string)] = true 92 | context.Sender.Tell(context, "helloback", "hello "+name+"!", context.Self) 93 | } 94 | }) 95 | ActorSystem().SpawnActor(&parentActor, "child", &childActor, nil) 96 | 97 | childActorRef, _ := ActorSystem().ActorOf("child") 98 | parentActorRef, _ := ActorSystem().ActorOf("parent") 99 | 100 | childActorRef.Tell(EmptyContext, "hello", "teivah", parentActorRef) 101 | childActorRef.Tell(EmptyContext, "hello", "teivah", parentActorRef) 102 | 103 | time.Sleep(250 * time.Millisecond) 104 | } 105 | 106 | func TestForward(t *testing.T) { 107 | t.Log("Starting forward test") 108 | 109 | opts := SystemOptions{ 110 | ActorSystemName: "ActorSystem", 111 | } 112 | InitActorSystem(opts) 113 | defer CloseActorSystem() 114 | 115 | parentActor := Actor{} 116 | defer parentActor.Close() 117 | ActorSystem().RegisterActor("parentActor", &parentActor, nil) 118 | 119 | forwarderActor := Actor{} 120 | defer forwarderActor.Close() 121 | forwarderActor.React("context", func(context Context) { 122 | context.Self.LogInfo(context, "Received %v\n", context.Data) 123 | context.Self.Forward(context, "childActor1", "childActor2") 124 | }) 125 | ActorSystem().SpawnActor(&parentActor, "forwarderActor", &forwarderActor, nil) 126 | 127 | childActor1 := Actor{} 128 | defer childActor1.Close() 129 | childActor1.React("context", func(context Context) { 130 | context.Self.LogInfo(context, "Received %v from %v\n", context.Data, context.Sender) 131 | }) 132 | ActorSystem().SpawnActor(&forwarderActor, "childActor1", &childActor1, nil) 133 | 134 | childActor2 := Actor{} 135 | defer childActor2.Close() 136 | childActor2.React("context", func(context Context) { 137 | context.Self.LogInfo(context, "Received %v from %v\n", context.Data, context.Sender) 138 | }) 139 | ActorSystem().SpawnActor(&forwarderActor, "childActor2", &childActor2, nil) 140 | 141 | parentActorRef, _ := ActorSystem().ActorOf("parentActor") 142 | forwarderActorRef, _ := ActorSystem().ActorOf("forwarderActor") 143 | 144 | forwarderActorRef.Tell(EmptyContext, "context", "to be forwarded", parentActorRef) 145 | time.Sleep(1500 * time.Millisecond) 146 | } 147 | 148 | func TestBecomeUnbecome(t *testing.T) { 149 | t.Log("Starting become/unbecome test") 150 | 151 | opts := SystemOptions{ 152 | ActorSystemName: "ActorSystem", 153 | } 154 | InitActorSystem(opts) 155 | defer CloseActorSystem() 156 | 157 | angry := func(context Context) { 158 | if context.Data == "happy" { 159 | context.Self.LogInfo(context, "Unbecome\n") 160 | context.Self.Unbecome(context.MessageType) 161 | } else { 162 | context.Self.LogInfo(context, "Angrily receiving %v\n", context.Data) 163 | } 164 | } 165 | 166 | happy := func(context Context) { 167 | if context.Data == "angry" { 168 | context.Self.LogInfo(context, "I shall become angry\n") 169 | context.Self.Become(context.MessageType, angry) 170 | } else { 171 | context.Self.LogInfo(context, "Happily receiving %v\n", context.Data) 172 | } 173 | } 174 | 175 | parentActor := Actor{} 176 | defer parentActor.Close() 177 | ActorSystem().RegisterActor("parentActor", &parentActor, nil) 178 | 179 | childActor := Actor{} 180 | defer childActor.Close() 181 | childActor.React("context", happy) 182 | ActorSystem().SpawnActor(&parentActor, "childActor", &childActor, nil) 183 | 184 | parentActorRef, _ := ActorSystem().ActorOf("parentActor") 185 | childActorRef, _ := ActorSystem().ActorOf("childActor") 186 | 187 | childActorRef.Tell(EmptyContext, "context", "hello!", parentActorRef) 188 | childActorRef.Tell(EmptyContext, "context", "angry", parentActorRef) 189 | childActorRef.Tell(EmptyContext, "context", "hello!", parentActorRef) 190 | childActorRef.Tell(EmptyContext, "context", "happy", parentActorRef) 191 | childActorRef.Tell(EmptyContext, "context", "hello!", parentActorRef) 192 | time.Sleep(500 * time.Millisecond) 193 | } 194 | 195 | func TestAmqp(t *testing.T) { 196 | t.Log("Starting AMQP test") 197 | time.Sleep(500 * time.Millisecond) 198 | 199 | opts := SystemOptions{ 200 | ActorSystemName: "ActorSystem", 201 | RegistryUrl: "http://etcd:2379", 202 | } 203 | InitActorSystem(opts) 204 | defer CloseActorSystem() 205 | 206 | actor1 := new(Actor).React("reply", func(context Context) { 207 | context.Self.LogInfo(context, "Received %v", context.Data) 208 | }) 209 | defer actor1.Close() 210 | ActorSystem().RegisterActor("actorX", actor1, new(ActorOptions).SetRemote(true).SetRemoteType(Amqp).SetUrl("amqp://guest:guest@amqp:5672/").SetDestination("actor1")) 211 | 212 | actor2 := new(Actor).React("context", func(context Context) { 213 | context.Self.LogInfo(context, "Received %v", context.Data) 214 | context.Sender.Tell(context, "reply", "hello back", context.Self) 215 | }) 216 | defer actor2.Close() 217 | ActorSystem().RegisterActor("actorY", actor2, new(ActorOptions).SetRemote(true).SetRemoteType(Amqp).SetUrl("amqp://guest:guest@amqp:5672/").SetDestination("actor2")) 218 | 219 | actorRef1, _ := ActorSystem().ActorOf("actorX") 220 | actorRef2, _ := ActorSystem().ActorOf("actorY") 221 | 222 | actorRef2.Tell(EmptyContext, "context", "hello", actorRef1) 223 | time.Sleep(500 * time.Millisecond) 224 | } 225 | 226 | func TestKafka(t *testing.T) { 227 | t.Log("Starting Kafka test") 228 | 229 | opts := SystemOptions{ 230 | ActorSystemName: "ActorSystem", 231 | RegistryUrl: "http://etcd:2379", 232 | } 233 | InitActorSystem(opts) 234 | defer CloseActorSystem() 235 | 236 | actor1 := new(Actor).React("reply", func(context Context) { 237 | context.Self.LogInfo(context, "Received %v", context.Data) 238 | }) 239 | defer actor1.Close() 240 | ActorSystem().RegisterActor("actorX", actor1, new(ActorOptions).SetRemote(true).SetRemoteType(Kafka).SetUrl("kafka:9092").SetDestination("actor1")) 241 | 242 | actor2 := new(Actor).React("context", func(context Context) { 243 | context.Self.LogInfo(context, "Received %v", context.Data) 244 | context.Sender.Tell(EmptyContext, "reply", "hello back", context.Self) 245 | }) 246 | defer actor2.Close() 247 | ActorSystem().RegisterActor("actorY", actor2, new(ActorOptions).SetRemote(true).SetRemoteType(Kafka).SetUrl("kafka:9092").SetDestination("actor2")) 248 | 249 | actorRef1, _ := ActorSystem().ActorOf("actorX") 250 | actorRef2, _ := ActorSystem().ActorOf("actorY") 251 | 252 | actorRef2.Tell(EmptyContext, "context", "hello", actorRef1) 253 | time.Sleep(2500 * time.Millisecond) 254 | } 255 | 256 | func TestAmqpKafka(t *testing.T) { 257 | t.Log("Starting AMQP/Kakfa test") 258 | 259 | opts := SystemOptions{ 260 | ActorSystemName: "ActorSystem", 261 | RegistryUrl: "http://etcd:2379", 262 | ZipkinOptions: ZipkinOptions{ 263 | Url: "http://zipkin:9411/api/v1/spans", 264 | Debug: true, 265 | HostPort: "0.0.0.0", 266 | SameSpan: true, 267 | }, 268 | } 269 | 270 | InitActorSystem(opts) 271 | defer CloseActorSystem() 272 | 273 | actor1 := new(Actor).React("reply", func(context Context) { 274 | context.Self.LogInfo(context, "Received1 %v", context.Data) 275 | 276 | }) 277 | defer actor1.Close() 278 | ActorSystem().RegisterActor("amqpActor", actor1, new(ActorOptions).SetRemote(true).SetRemoteType(Amqp).SetUrl("amqp://guest:guest@amqp:5672/").SetDestination("actor1")) 279 | 280 | actor2 := new(Actor).React("context", func(context Context) { 281 | context.Self.LogInfo(context, "Received2 %v", context.Data) 282 | context.Sender.Tell(context, "reply", "hello back", context.Self) 283 | }) 284 | defer actor2.Close() 285 | ActorSystem().SpawnActor(actor1, "kafkaActor", actor2, new(ActorOptions).SetRemote(true).SetRemoteType(Kafka).SetUrl("kafka:9092").SetDestination("actor2")) 286 | 287 | amqpRef, _ := ActorSystem().ActorOf("amqpActor") 288 | kafkaRef, _ := ActorSystem().ActorOf("kafkaActor") 289 | 290 | kafkaRef.Tell(EmptyContext, "context", "hello", amqpRef) 291 | time.Sleep(1500 * time.Millisecond) 292 | } 293 | 294 | func TestRemoteClose(t *testing.T) { 295 | t.Log("Starting remote close test") 296 | 297 | opts := SystemOptions{ 298 | ActorSystemName: "ActorSystem", 299 | RegistryUrl: "http://etcd:2379", 300 | } 301 | InitActorSystem(opts) 302 | defer CloseActorSystem() 303 | 304 | actorY := new(Actor).React("hello", func(context Context) { 305 | context.Self.LogInfo(context, "Received %v", context.Data) 306 | context.Sender.Tell(context, "reply", fmt.Sprintf("Hello %v", context.Data), context.Self) 307 | }) 308 | defer actorY.Close() 309 | ActorSystem().RegisterActor("actorY", actorY, new(ActorOptions).SetRemote(true).SetRemoteType("amqp").SetUrl("amqp://guest:guest@amqp:5672/").SetDestination("actor2")) 310 | 311 | a, _ := ActorSystem().ActorOf("actorY") 312 | a.AskForClose(a) 313 | time.Sleep(500 * time.Millisecond) 314 | } 315 | 316 | func TestAutocloseTrue(t *testing.T) { 317 | t.Log("Starting auto close true test") 318 | 319 | opts := SystemOptions{ 320 | ActorSystemName: "ActorSystem", 321 | } 322 | InitActorSystem(opts) 323 | defer CloseActorSystem() 324 | 325 | actorY := new(Actor) 326 | defer actorY.Close() 327 | ActorSystem().RegisterActor("actorY", actorY, new(ActorOptions).SetAutoclose(false)) 328 | a, _ := ActorSystem().ActorOf("actorY") 329 | 330 | a.AskForClose(a) 331 | time.Sleep(500 * time.Millisecond) 332 | } 333 | 334 | func TestAutocloseFalse(t *testing.T) { 335 | t.Log("Starting auto close false test") 336 | 337 | opts := SystemOptions{ 338 | ActorSystemName: "ActorSystem", 339 | } 340 | InitActorSystem(opts) 341 | defer CloseActorSystem() 342 | 343 | actorY := new(Actor) 344 | actorY.React(GosirisMsgPoisonPill, func(context Context) { 345 | context.Self.LogInfo(context, "Received a poison pill, closing.") 346 | actorY.Close() 347 | }) 348 | 349 | ActorSystem().RegisterActor("actorY", actorY, new(ActorOptions).SetAutoclose(false)) 350 | a, _ := ActorSystem().ActorOf("actorY") 351 | 352 | a.AskForClose(a) 353 | time.Sleep(500 * time.Millisecond) 354 | } 355 | 356 | func TestChildClosedNotificationLocal(t *testing.T) { 357 | t.Log("Starting child closed notification test") 358 | 359 | opts := SystemOptions{ 360 | ActorSystemName: "ActorSystem", 361 | } 362 | InitActorSystem(opts) 363 | defer CloseActorSystem() 364 | 365 | actorParent := new(Actor) 366 | defer actorParent.Close() 367 | actorParent.React(GosirisMsgChildClosed, func(context Context) { 368 | context.Self.LogInfo(context, "My child is closed") 369 | }) 370 | 371 | actorChild := new(Actor) 372 | actorChild.React("do", func(context Context) { 373 | if context.Data == 0 { 374 | context.Self.LogInfo(context, "I feel like being closed") 375 | actorChild.Close() 376 | } else { 377 | context.Self.LogInfo(context, "Received %v", context.Data) 378 | } 379 | }) 380 | 381 | ActorSystem().RegisterActor("ActorParent", actorParent, nil) 382 | ActorSystem().SpawnActor(actorParent, "ActorChild", actorChild, nil) 383 | 384 | actorParentRef, _ := ActorSystem().ActorOf("ActorParent") 385 | actorChildRef, _ := ActorSystem().ActorOf("ActorChild") 386 | 387 | actorChildRef.Tell(EmptyContext, "do", 1, actorParentRef) 388 | actorChildRef.Tell(EmptyContext, "do", 0, actorParentRef) 389 | 390 | time.Sleep(500 * time.Millisecond) 391 | } 392 | 393 | func TestChildClosedNotificationRemote(t *testing.T) { 394 | t.Log("Starting child closed notification remote test") 395 | 396 | opts := SystemOptions{ 397 | ActorSystemName: "ActorSystem", 398 | RegistryUrl: "http://etcd:2379", 399 | } 400 | InitActorSystem(opts) 401 | defer CloseActorSystem() 402 | 403 | actorParent := new(Actor) 404 | defer actorParent.Close() 405 | actorParent.React(GosirisMsgChildClosed, func(context Context) { 406 | context.Self.LogInfo(context, "My child is closed") 407 | }) 408 | 409 | actorChild := new(Actor) 410 | actorChild.React("do", func(context Context) { 411 | if context.Data == "0" { 412 | context.Self.LogInfo(context, "I feel like being closed") 413 | actorChild.Close() 414 | } else { 415 | context.Self.LogInfo(context, "Received %v", context.Data) 416 | } 417 | }) 418 | 419 | ActorSystem().RegisterActor("ActorParent", actorParent, new(ActorOptions).SetRemote(true).SetRemoteType("amqp").SetUrl("amqp://guest:guest@amqp:5672/").SetDestination("actorParent")) 420 | ActorSystem().SpawnActor(actorParent, "ActorChild", actorChild, new(ActorOptions).SetRemote(true).SetRemoteType("amqp").SetUrl("amqp://guest:guest@amqp:5672/").SetDestination("actorChild")) 421 | 422 | actorParentRef, _ := ActorSystem().ActorOf("ActorParent") 423 | actorChildRef, _ := ActorSystem().ActorOf("ActorChild") 424 | 425 | actorChildRef.Tell(EmptyContext, "do", 1, actorParentRef) 426 | actorChildRef.Tell(EmptyContext, "do", 0, actorParentRef) 427 | 428 | time.Sleep(1000 * time.Millisecond) 429 | } 430 | 431 | func TestRepeat(t *testing.T) { 432 | t.Log("Starting repeat test") 433 | 434 | opts := SystemOptions{ 435 | ActorSystemName: "ActorSystem", 436 | } 437 | InitActorSystem(opts) 438 | defer CloseActorSystem() 439 | 440 | parentActor := Actor{} 441 | defer parentActor.Close() 442 | 443 | ActorSystem().RegisterActor("parentActor", &parentActor, nil) 444 | 445 | childActor := Actor{} 446 | defer childActor.Close() 447 | 448 | childActor.React("context", func(context Context) { 449 | context.Self.LogInfo(context, "Received %v\n", context.Data) 450 | }) 451 | 452 | ActorSystem().SpawnActor(&parentActor, "childActor", &childActor, nil) 453 | 454 | parentActorRef, _ := ActorSystem().ActorOf("parentActor") 455 | childActorRef, _ := ActorSystem().ActorOf("childActor") 456 | 457 | c, _ := childActorRef.Repeat("context", 5*time.Millisecond, "Hi! How are you?", parentActorRef) 458 | 459 | time.Sleep(21 * time.Millisecond) 460 | 461 | ActorSystem().Stop(c) 462 | time.Sleep(500 * time.Millisecond) 463 | } 464 | 465 | //TODO 466 | func TestDefaultWatcher(t *testing.T) { 467 | t.Log("Starting default watcher test") 468 | 469 | opts := SystemOptions{ 470 | ActorSystemName: "ActorSystem", 471 | } 472 | InitActorSystem(opts) 473 | defer CloseActorSystem() 474 | 475 | parentActor := Actor{} 476 | defer parentActor.Close() 477 | 478 | ActorSystem().RegisterActor("parentActor", &parentActor, nil) 479 | 480 | childActor := Actor{} 481 | defer childActor.Close() 482 | 483 | ActorSystem().SpawnActor(&parentActor, "childActor", &childActor, new(ActorOptions).SetDefaultWatcher(5*time.Millisecond)) 484 | 485 | time.Sleep(21 * time.Millisecond) 486 | } 487 | -------------------------------------------------------------------------------- /gosiris/dispatcher.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/opentracing/opentracing-go" 7 | ) 8 | 9 | func init() { 10 | 11 | } 12 | 13 | var ( 14 | EmptyContext Context = Context{} 15 | ) 16 | 17 | const ( 18 | GosirisMsgPoisonPill = "gosirisPoisonPill" 19 | GosirisMsgChildClosed = "gosirisChildClosed" 20 | GosirisMsgHeartbeatRequest = "gosirisHeartbeatRequest" 21 | GosirisMsgHeartbeatReply = "gosirisHeartbeatReply" 22 | 23 | jsonMessageType = "messageType" 24 | jsonData = "data" 25 | jsonSender = "sender" 26 | jsonSelf = "self" 27 | jsonTracing = "tracing" 28 | ) 29 | 30 | type Context struct { 31 | MessageType string 32 | Data interface{} 33 | Sender ActorRefInterface 34 | Self ActorRefInterface 35 | carrier opentracing.TextMapCarrier 36 | span opentracing.Span 37 | } 38 | 39 | func (context Context) MarshalJSON() ([]byte, error) { 40 | m := make(map[string]interface{}) 41 | m[jsonMessageType] = context.MessageType 42 | m[jsonData] = fmt.Sprint(context.Data) 43 | m[jsonSender] = context.Sender.Name() 44 | m[jsonSelf] = context.Self.Name() 45 | m[jsonTracing] = context.carrier 46 | return json.Marshal(m) 47 | } 48 | 49 | func (context *Context) UnmarshalJSON(b []byte) error { 50 | var m map[string]interface{} 51 | err := json.Unmarshal(b, &m) 52 | if err != nil { 53 | ErrorLogger.Printf("Unmarshalling error: %v", err) 54 | return err 55 | } 56 | 57 | context.MessageType = m[jsonMessageType].(string) 58 | 59 | context.Data = m[jsonData] 60 | 61 | self := m[jsonSelf].(string) 62 | selfAssociation, err := ActorSystem().actor(self) 63 | if err != nil { 64 | return err 65 | } 66 | context.Self = selfAssociation.actorRef 67 | 68 | sender := m[jsonSender].(string) 69 | senderAssociation, err := ActorSystem().actor(sender) 70 | if err != nil { 71 | return err 72 | } 73 | context.Sender = senderAssociation.actorRef 74 | 75 | if value, exists := m[jsonTracing]; exists { 76 | if value != nil { 77 | t := value.(map[string]interface{}) 78 | context.carrier = make(map[string]string) 79 | 80 | for k, v := range t { 81 | context.carrier[k] = v.(string) 82 | } 83 | } 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func dispatch(channel chan Context, messageType string, data interface{}, receiver ActorRefInterface, sender ActorRefInterface, options OptionsInterface, span opentracing.Span) error { 90 | defer func() { 91 | if r := recover(); r != nil { 92 | ErrorLogger.Printf("Dispatch recovered in %v", r) 93 | } 94 | }() 95 | 96 | InfoLogger.Printf("Dispatching message %v from %v to %v", messageType, sender.Name(), receiver.Name()) 97 | 98 | carrier, _ := inject(span) 99 | m := Context{messageType, data, sender, receiver, carrier, nil} 100 | 101 | if !options.Remote() { 102 | channel <- m 103 | InfoLogger.Printf("Context dispatched to local channel") 104 | } else { 105 | d, err := RemoteConnection(receiver.Name()) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | json, err := json.Marshal(m) 111 | if err != nil { 112 | ErrorLogger.Printf("JSON marshalling error: %v", err) 113 | return err 114 | } 115 | 116 | d.Send(options.Destination(), json) 117 | InfoLogger.Printf("Context dispatched to remote channel %v", options.Destination()) 118 | } 119 | 120 | return nil 121 | } 122 | 123 | func receive(actor actorInterface, options OptionsInterface) { 124 | if !options.Remote() { 125 | defer func() { 126 | if r := recover(); r != nil { 127 | ErrorLogger.Printf("Receive recovered in %v", r) 128 | } 129 | }() 130 | 131 | dataChan := actor.getDataChan() 132 | closeChan := actor.getCloseChan() 133 | for { 134 | select { 135 | case p := <-dataChan: 136 | ActorSystem().Invoke(p) 137 | case <-closeChan: 138 | InfoLogger.Printf("Closing %v receiver", actor.Name()) 139 | close(dataChan) 140 | close(closeChan) 141 | return 142 | } 143 | } 144 | } else { 145 | d, err := RemoteConnection(actor.Name()) 146 | if err != nil { 147 | return 148 | } 149 | 150 | d.Receive(options.Destination()) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /gosiris/etcd.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import ( 4 | "context" 5 | "github.com/coreos/etcd/client" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | const ( 11 | actors_configuration = "/gosiris/actor/" 12 | prefix = "gosiris://" 13 | delimiter = "#" 14 | action_delete = "delete" 15 | action_set = "set" 16 | ) 17 | 18 | type etcdClient struct { 19 | api client.KeysAPI 20 | } 21 | 22 | func (etcdClient *etcdClient) Configure(url ...string) error { 23 | cfg := client.Config{ 24 | Endpoints: url, 25 | Transport: client.DefaultTransport, 26 | HeaderTimeoutPerRequest: time.Second, 27 | } 28 | 29 | c, err := client.New(cfg) 30 | if err != nil { 31 | ErrorLogger.Printf("etcd connection error %v", err) 32 | return err 33 | } 34 | etcdClient.api = client.NewKeysAPI(c) 35 | etcdClient.createDir(actors_configuration) 36 | 37 | return nil 38 | } 39 | 40 | func (etcdClient *etcdClient) Close() { 41 | } 42 | 43 | func (etcdClient *etcdClient) Watch(cbCreate func(string, *ActorOptions), cbDelete func(string)) error { 44 | w := etcdClient.api.Watcher(actors_configuration, &client.WatcherOptions{ 45 | AfterIndex: 0, 46 | Recursive: true, 47 | }) 48 | 49 | for { 50 | r, err := w.Next(context.Background()) 51 | 52 | if err != nil { 53 | ErrorLogger.Printf("etcd watch error: %v", err) 54 | return err 55 | } 56 | 57 | if r.Action == action_delete { 58 | k := r.Node.Key 59 | InfoLogger.Printf("Actor %v removed from the registry", k) 60 | 61 | cbDelete(k) 62 | } else if r.Action == action_set { 63 | k, v := parseNode(r.Node) 64 | 65 | InfoLogger.Printf("New actor %v added to the registry", k) 66 | 67 | cbCreate(k, v) 68 | } 69 | } 70 | } 71 | 72 | func parseNode(node *client.Node) (string, *ActorOptions) { 73 | v := node.Value 74 | a := 75 | strings.Split(v, delimiter) 76 | k := node.Key 77 | 78 | return k[len(actors_configuration):], &ActorOptions{a[0], true, true, a[1], a[2], a[3], 0, 0} 79 | } 80 | 81 | func (etcdClient *etcdClient) ParseConfiguration() (map[string]OptionsInterface, error) { 82 | resp, err := etcdClient.Get(actors_configuration) 83 | 84 | if err != nil { 85 | return nil, nil 86 | } 87 | 88 | conf := make(map[string]OptionsInterface) 89 | 90 | nodes := resp.Node.Nodes 91 | for i := 0; i < nodes.Len(); i++ { 92 | k, v := parseNode(nodes[i]) 93 | conf[k] = v 94 | } 95 | 96 | return conf, nil 97 | } 98 | 99 | func (etcdClient *etcdClient) RegisterActor(name string, options OptionsInterface) error { 100 | k := actors_configuration + name 101 | v := options.Parent() + delimiter + options.RemoteType() + delimiter + options.Url() + delimiter + options.Destination() 102 | 103 | err := etcdClient.Set(k, v) 104 | if err != nil { 105 | ErrorLogger.Printf("Failed to register actor %v: %v", k, err) 106 | return err 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func (etcdClient *etcdClient) UnregisterActor(name string) error { 113 | return etcdClient.Delete(actors_configuration + name) 114 | } 115 | 116 | func (etcdClient *etcdClient) createDir(key string) { 117 | opt := new(client.SetOptions) 118 | opt.Dir = true 119 | 120 | etcdClient.api.Set(context.Background(), key, "", opt) 121 | } 122 | 123 | func (etcdClient *etcdClient) Set(key string, value string) error { 124 | _, err := etcdClient.api.Set(context.Background(), key, value, nil) 125 | 126 | if err != nil { 127 | ErrorLogger.Printf("etcd set %v error %v", key, err) 128 | } 129 | 130 | return err 131 | } 132 | 133 | func (etcdClient *etcdClient) Delete(key string) error { 134 | _, err := etcdClient.api.Delete(context.Background(), key, nil) 135 | 136 | return err 137 | } 138 | 139 | func (etcdClient *etcdClient) Get(key string) (*client.Response, error) { 140 | resp, err := etcdClient.api.Get(context.Background(), key, &client.GetOptions{ 141 | Recursive: false, 142 | Sort: false, 143 | Quorum: false, 144 | }) 145 | 146 | if err != nil { 147 | ErrorLogger.Printf("etcd get %v error %v", key, err) 148 | return resp, err 149 | } 150 | 151 | return resp, err 152 | } 153 | 154 | func (etcdClient *etcdClient) GetValue(key string) (string, error) { 155 | resp, err := etcdClient.Get(key) 156 | 157 | return resp.Node.Value, err 158 | } 159 | -------------------------------------------------------------------------------- /gosiris/log.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | var InfoLogger *log.Logger 9 | var ErrorLogger *log.Logger 10 | var FatalLogger *log.Logger 11 | 12 | func init() { 13 | InfoLogger = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile) 14 | ErrorLogger = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile) 15 | FatalLogger = log.New(os.Stderr, "FATAL: ", log.Ldate|log.Ltime|log.Lshortfile) 16 | } 17 | 18 | func NewActorLogger(name string) (*log.Logger, *log.Logger) { 19 | return log.New(os.Stdout, "INFO: ["+name+"] ", log.Ldate|log.Ltime), log.New(os.Stderr, "ERROR: ["+name+"] ", log.Ldate|log.Ltime) 20 | } 21 | -------------------------------------------------------------------------------- /gosiris/registry.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | type registryInterface interface { 4 | Configure(...string) error 5 | Close() 6 | RegisterActor(string, OptionsInterface) error 7 | Watch(func(string, *ActorOptions), func(string)) error 8 | UnregisterActor(string) error 9 | ParseConfiguration() (map[string]OptionsInterface, error) 10 | } 11 | -------------------------------------------------------------------------------- /gosiris/system.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import ( 4 | "fmt" 5 | "github.com/opentracing/opentracing-go" 6 | "time" 7 | ) 8 | 9 | var root *Actor 10 | var actorSystemInstance actorSystem 11 | var actorSystemStarted bool 12 | var registry registryInterface 13 | 14 | type SystemOptions struct { 15 | ActorSystemName string 16 | ZipkinOptions ZipkinOptions 17 | RegistryUrl string 18 | } 19 | 20 | func init() { 21 | root = &Actor{} 22 | root.name = "root" 23 | actorSystemStarted = false 24 | } 25 | 26 | func InitActorSystem(options SystemOptions) error { 27 | if actorSystemStarted { 28 | ErrorLogger.Printf("Actor system already started") 29 | return fmt.Errorf("actor system already started") 30 | } 31 | 32 | actorSystemInstance = actorSystem{} 33 | actorSystemInstance.actors = make(map[string]actorAssociation) 34 | actorSystemStarted = true 35 | 36 | if options.RegistryUrl != "" { 37 | initDistributedActorSystem(options.RegistryUrl) 38 | } 39 | 40 | if options.ZipkinOptions.Url != "" { 41 | initZipkinSystem(options.ActorSystemName, options.ZipkinOptions) 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func initDistributedActorSystem(url string) error { 48 | //TODO Manage the implementation dynamically 49 | registry = &etcdClient{} 50 | err := registry.Configure(url) 51 | if err != nil { 52 | actorSystemStarted = false 53 | FatalLogger.Fatalf("Failed to configure access to the remote repository: %v", err) 54 | } 55 | 56 | conf, err := registry.ParseConfiguration() 57 | if err != nil { 58 | actorSystemStarted = false 59 | FatalLogger.Fatalf("Failed to parse the configuration: %v", err) 60 | } 61 | 62 | InitRemoteConnections(conf) 63 | ActorSystem().addRemoteActors(conf) 64 | 65 | return nil 66 | } 67 | 68 | func ActorSystem() *actorSystem { 69 | return &actorSystemInstance 70 | } 71 | 72 | func CloseActorSystem() error { 73 | if !actorSystemStarted { 74 | ErrorLogger.Printf("Actor system not started") 75 | return fmt.Errorf("actor system not started") 76 | } 77 | 78 | if registry != nil { 79 | registry.Close() 80 | } 81 | InfoLogger.Printf("Actor system closed") 82 | 83 | actorSystemStarted = false 84 | closeZipkinSystem() 85 | 86 | return nil 87 | } 88 | 89 | type actorAssociation struct { 90 | actorRef ActorRefInterface 91 | actor actorInterface 92 | options OptionsInterface 93 | } 94 | 95 | type actorSystem struct { 96 | actors map[string]actorAssociation 97 | } 98 | 99 | func (system *actorSystem) RegisterActor(name string, actor actorInterface, options OptionsInterface) error { 100 | if name == root.name { 101 | ErrorLogger.Printf("Register an actor whose name is %v is not allowed", name) 102 | return fmt.Errorf("register an actor whose name is %v is not allowed", name) 103 | } 104 | 105 | InfoLogger.Printf("Registering new actor %v", name) 106 | return system.SpawnActor(RootActor(), name, actor, options) 107 | } 108 | 109 | func (system *actorSystem) SpawnActor(parent actorInterface, name string, actor actorInterface, options OptionsInterface) error { 110 | InfoLogger.Printf("Spawning new actor %v", name) 111 | 112 | _, exists := system.actors[name] 113 | if exists { 114 | InfoLogger.Printf("Actor %v already registered", name) 115 | } 116 | 117 | if options == nil { 118 | options = &ActorOptions{} 119 | options.SetRemote(false) 120 | } 121 | 122 | if options.BufferSize() == 0 { 123 | options.SetBufferSize(defaultBufferSize) 124 | } 125 | 126 | actor.setName(name) 127 | actor.setParent(parent) 128 | options.setParent(parent.Name()) 129 | if !options.Remote() { 130 | actor.setDataChan(make(chan Context, options.BufferSize())) 131 | actor.setCloseChan(make(chan interface{})) 132 | } else { 133 | registry.RegisterActor(name, options) 134 | go registry.Watch(system.onActorCreatedFromRegistry, system.onActorRemovedFromRegistry) 135 | AddConnection(name, options) 136 | } 137 | 138 | actorRef := newActorRef(name) 139 | 140 | system.actors[name] = 141 | actorAssociation{actorRef, actor, options} 142 | 143 | go receive(actor, options) 144 | 145 | if options.DefaultWatcher() != 0 { 146 | //Configure a default watcher 147 | t := time.NewTicker(options.DefaultWatcher()) 148 | go func(t *time.Ticker, actorRef ActorRefInterface) { 149 | for { 150 | select { 151 | case <-t.C: 152 | p, err := system.actor(parent.Name()) 153 | if err != nil { 154 | ErrorLogger.Printf("Parent of actor %v not found", name) 155 | } 156 | dispatch(p.actor.getDataChan(), GosirisMsgHeartbeatRequest, nil, actorRef, p.actorRef, new(ActorOptions), nil) 157 | } 158 | } 159 | }(t, actorRef) 160 | } 161 | 162 | return nil 163 | } 164 | 165 | func (system *actorSystem) onActorCreatedFromRegistry(name string, options *ActorOptions) { 166 | actorRef := newActorRef(name) 167 | actor := Actor{} 168 | actor.name = name 169 | 170 | system.actors[name] = 171 | actorAssociation{actorRef, nil, options} 172 | 173 | AddConnection(name, options) 174 | 175 | InfoLogger.Printf("Actor %v added to the local system", name) 176 | } 177 | 178 | func (system *actorSystem) onActorRemovedFromRegistry(name string) { 179 | system.removeRemoteActor(name) 180 | 181 | InfoLogger.Printf("Actor %v removed from the local system", name) 182 | } 183 | 184 | func (system *actorSystem) removeRemoteActor(name string) { 185 | InfoLogger.Printf("Removing remote actor %v", name) 186 | 187 | v, err := system.actor(name) 188 | 189 | if err != nil { 190 | InfoLogger.Printf("Actor %v not registered", name) 191 | return 192 | } 193 | 194 | if v.options.Remote() { 195 | DeleteRemoteActorConnection(name) 196 | } 197 | 198 | delete(system.actors, name) 199 | 200 | InfoLogger.Printf("Remote actor %v removed", name) 201 | } 202 | 203 | func (system *actorSystem) closeLocalActor(name string) { 204 | InfoLogger.Printf("Closing local actor %v", name) 205 | 206 | v, err := system.actor(name) 207 | 208 | if err != nil { 209 | ErrorLogger.Printf("Unable to close actor %v", name) 210 | return 211 | } 212 | 213 | //If the actor has a parent we send him a message 214 | if v.actor.Parent() != nil { 215 | parentName := v.actor.Parent().Name() 216 | if parentName != root.name { 217 | p, err := system.actor(parentName) 218 | if err != nil { 219 | ErrorLogger.Printf("Parent %v not registered", parentName) 220 | } 221 | p.actorRef.Tell(EmptyContext, GosirisMsgChildClosed, name, v.actorRef) 222 | } 223 | } 224 | 225 | //m := v.actor.getDataChan() 226 | //if m != nil { 227 | // close(m) 228 | //} 229 | 230 | c := v.actor.getCloseChan() 231 | if c != nil { 232 | c <- 0 233 | } 234 | 235 | if registry != nil { 236 | registry.UnregisterActor(name) 237 | } 238 | 239 | delete(system.actors, name) 240 | 241 | InfoLogger.Printf("%v unregistered from the actor system", name) 242 | } 243 | 244 | func (system *actorSystem) actor(name string) (actorAssociation, error) { 245 | ref, exists := system.actors[name] 246 | if !exists { 247 | return actorAssociation{}, fmt.Errorf("actor %v not registered", name) 248 | } 249 | 250 | return ref, nil 251 | } 252 | 253 | func (system *actorSystem) addRemoteActors(configuration map[string]OptionsInterface) { 254 | for k, v := range configuration { 255 | actor := Actor{} 256 | actor.setName(k) 257 | actorRef := newActorRef(k) 258 | 259 | system.actors[k] = actorAssociation{actorRef, &actor, v} 260 | } 261 | 262 | InfoLogger.Printf("Actors configuration: %v", system.actors) 263 | } 264 | 265 | func (system *actorSystem) ActorOf(name string) (ActorRefInterface, error) { 266 | actorAssociation, err := system.actor(name) 267 | 268 | if err != nil { 269 | return nil, err 270 | } 271 | 272 | return actorAssociation.actorRef, err 273 | } 274 | 275 | func (system *actorSystem) Invoke(message Context) error { 276 | if message.Self == nil { 277 | return nil 278 | } 279 | 280 | InfoLogger.Printf("Invoking %v", message) 281 | 282 | actorAssociation, err := system.actor(message.Self.Name()) 283 | 284 | if err != nil { 285 | ErrorLogger.Printf("Invoke error: actor %v not registered", actorAssociation.actorRef.Name()) 286 | return err 287 | } 288 | 289 | if message.MessageType == GosirisMsgPoisonPill { 290 | InfoLogger.Printf("Actor %v has received a poison pill", actorAssociation.actor.Name()) 291 | 292 | if actorAssociation.options.Autoclose() { 293 | InfoLogger.Printf("Autoclose actor %v", actorAssociation.actor.Name()) 294 | actorAssociation.actor.Close() 295 | return nil 296 | } 297 | } else if message.MessageType == GosirisMsgHeartbeatRequest { 298 | InfoLogger.Printf("Actor %v has received a heartbeat request", actorAssociation.actor.Name()) 299 | 300 | message.Sender.Tell(EmptyContext, GosirisMsgHeartbeatReply, nil, message.Self) 301 | } 302 | 303 | if actorAssociation.actor.reactions() != nil { 304 | f, exists := 305 | actorAssociation.actor.reactions()[message.MessageType] 306 | if exists { 307 | var span opentracing.Span 308 | if message.carrier != nil { 309 | ctx, _ := extract(message.carrier) 310 | span = tracer.StartSpan("operation", opentracing.ChildOf(ctx)) 311 | InfoLogger.Printf("Starting child span") 312 | message.span = span 313 | } 314 | f(message) 315 | if message.carrier != nil { 316 | span.Finish() 317 | } 318 | } 319 | } 320 | 321 | return nil 322 | } 323 | 324 | func (system *actorSystem) Stop(stop chan struct{}) { 325 | if stop != nil { 326 | stop <- struct{}{} 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /gosiris/transport.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var remoteConnections map[string]TransportInterface 8 | var transportTypes map[string]func() TransportInterface 9 | 10 | func init() { 11 | transportTypes = make(map[string]func() TransportInterface) 12 | } 13 | 14 | type TransportInterface interface { 15 | Configure(string, map[string]string) 16 | Connection() error 17 | Send(string, []byte) error 18 | Receive(string) 19 | Close() 20 | } 21 | 22 | func registerTransport(name string, f func() TransportInterface) { 23 | transportTypes[name] = f 24 | } 25 | 26 | func newTransport(name string) TransportInterface { 27 | return transportTypes[name]() 28 | } 29 | 30 | func InitRemoteConnections(configuration map[string]OptionsInterface) { 31 | remoteConnections = make(map[string]TransportInterface) 32 | 33 | for k, v := range configuration { 34 | c := transportTypes[v.RemoteType()]() 35 | 36 | c.Configure(v.Url(), nil) 37 | err := c.Connection() 38 | if err != nil { 39 | ErrorLogger.Printf("Failed to initialize the connection with %v: %v", v, err) 40 | } 41 | remoteConnections[k] = c 42 | } 43 | 44 | InfoLogger.Printf("Remote connections: %v", remoteConnections) 45 | } 46 | 47 | func AddConnection(name string, conf OptionsInterface) { 48 | c := transportTypes[conf.RemoteType()]() 49 | 50 | c.Configure(conf.Url(), nil) 51 | err := c.Connection() 52 | if err != nil { 53 | ErrorLogger.Printf("Failed to initialize the connection with %v: %v", name, err) 54 | } 55 | remoteConnections[name] = c 56 | InfoLogger.Printf("Remote connection %v added", name) 57 | } 58 | 59 | func DeleteRemoteActorConnection(name string) error { 60 | v, exists := remoteConnections[name] 61 | if !exists { 62 | ErrorLogger.Printf("Delete error: connection %v not registered", name) 63 | return fmt.Errorf("delete error: connection %v not registered", name) 64 | } 65 | 66 | v.Close() 67 | delete(remoteConnections, name) 68 | 69 | InfoLogger.Printf("Connection %v deleted", name) 70 | 71 | return nil 72 | } 73 | 74 | func RemoteConnection(name string) (TransportInterface, error) { 75 | v, exists := remoteConnections[name] 76 | if !exists { 77 | ErrorLogger.Printf("Remote connection error: connection %v not registered", name) 78 | return nil, fmt.Errorf("remote connection error: connection %v not registered", name) 79 | } 80 | 81 | return v, nil 82 | } 83 | -------------------------------------------------------------------------------- /gosiris/transport_amqp.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/streadway/amqp" 6 | ) 7 | 8 | var Amqp = "amqp" 9 | 10 | type amqpTransport struct { 11 | url string 12 | connection *amqp.Connection 13 | channel *amqp.Channel 14 | } 15 | 16 | func init() { 17 | registerTransport(Amqp, newAmqpTransport) 18 | } 19 | 20 | func newAmqpTransport() TransportInterface { 21 | return new(amqpTransport) 22 | } 23 | 24 | func (a *amqpTransport) Configure(url string, options map[string]string) { 25 | a.url = url 26 | } 27 | 28 | func (a *amqpTransport) Connection() error { 29 | c, err := amqp.Dial(a.url) 30 | if err != nil { 31 | ErrorLogger.Printf("Failed to connect to the AMQP server %v", a.url) 32 | return err 33 | } 34 | a.connection = c 35 | 36 | ch, err := c.Channel() 37 | if err != nil { 38 | ErrorLogger.Printf("Failed to open an AMQP channel on the server %v", a.url) 39 | return err 40 | } 41 | a.channel = ch 42 | 43 | InfoLogger.Printf("Connected to %v", a.url) 44 | 45 | return nil 46 | } 47 | 48 | func (a *amqpTransport) Receive(queueName string) { 49 | q, err := a.channel.QueueDeclare( 50 | queueName, // name 51 | false, // durable 52 | false, // delete when unused 53 | false, // exclusive 54 | false, // no-wait 55 | nil, // arguments 56 | ) 57 | 58 | if err != nil { 59 | ErrorLogger.Printf("Error while declaring queue %v: %v", queueName, err) 60 | } 61 | 62 | msgs, _ := a.channel.Consume( 63 | q.Name, // queue 64 | "", // consumer 65 | true, // auto-ack 66 | false, // exclusive 67 | false, // no-local 68 | false, // no-wait 69 | nil, // args 70 | ) 71 | for d := range msgs { 72 | msg := Context{} 73 | json.Unmarshal(d.Body, &msg) 74 | InfoLogger.Printf("New AMQP message received: %v", msg) 75 | ActorSystem().Invoke(msg) 76 | } 77 | } 78 | 79 | func (a *amqpTransport) Close() { 80 | a.channel.Close() 81 | a.connection.Close() 82 | } 83 | 84 | func (a *amqpTransport) Send(destination string, data []byte) error { 85 | InfoLogger.Printf("Sending message to the AMQP destination %v", destination) 86 | 87 | q, err := a.channel.QueueDeclare( 88 | destination, // name 89 | false, // durable 90 | false, // delete when unused 91 | false, // exclusive 92 | false, // no-wait 93 | nil, // arguments 94 | ) 95 | if err != nil { 96 | ErrorLogger.Printf("Error while declaring queue %v: %v", destination, err) 97 | return err 98 | } 99 | 100 | body := data 101 | err = a.channel.Publish( 102 | "", // exchange 103 | q.Name, // routing key 104 | false, // mandatory 105 | false, // immediate 106 | amqp.Publishing{ 107 | ContentType: "text/plain", 108 | Body: []byte(body), 109 | }) 110 | 111 | if err != nil { 112 | ErrorLogger.Printf("Error while publishing a message to queue %v: %v", destination, err) 113 | return err 114 | } 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /gosiris/transport_kafka.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/Shopify/sarama" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | var Kafka = "kafka" 12 | 13 | func init() { 14 | registerTransport(Kafka, newKafkaTransport) 15 | } 16 | 17 | func newKafkaTransport() TransportInterface { 18 | return new(kafkaTransport) 19 | } 20 | 21 | type kafkaTransport struct { 22 | url string 23 | producer sarama.AsyncProducer 24 | consumer sarama.Consumer 25 | } 26 | 27 | type accessLogEntry struct { 28 | Method string `json:"method"` 29 | Host string `json:"host"` 30 | Path string `json:"path"` 31 | IP string `json:"ip"` 32 | ResponseTime float64 `json:"response_time"` 33 | 34 | encoded []byte 35 | err error 36 | } 37 | 38 | func (k *kafkaTransport) Configure(url string, options map[string]string) { 39 | k.url = url 40 | } 41 | 42 | func (k *kafkaTransport) Connection() error { 43 | list := strings.Split(k.url, ",") 44 | 45 | producer, err := newProducer(list) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | consumer, err := newConsumer(list) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | k.producer = producer 56 | k.consumer = consumer 57 | 58 | return nil 59 | } 60 | 61 | func (k *kafkaTransport) Receive(queueName string) { 62 | consumer, err := k.consumer.ConsumePartition(queueName, 0, sarama.OffsetNewest) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | for { 68 | select { 69 | case err := <-consumer.Errors(): 70 | ErrorLogger.Printf("Kafka consumer error: %v", err) 71 | case message := <-consumer.Messages(): 72 | msg := EmptyContext 73 | json.Unmarshal(message.Value, &msg) 74 | InfoLogger.Printf("New Kafka message received: %v", msg) 75 | ActorSystem().Invoke(msg) 76 | } 77 | } 78 | } 79 | 80 | func (k *kafkaTransport) Close() { 81 | if err := k.producer.Close(); err != nil { 82 | ErrorLogger.Printf("Failed shut access log producer: %v", err) 83 | } 84 | } 85 | 86 | func (k *kafkaTransport) Send(destination string, data []byte) error { 87 | InfoLogger.Printf("Sending message to the Kafka destination %v", destination) 88 | k.producer.Input() <- &sarama.ProducerMessage{ 89 | Topic: destination, 90 | Value: sarama.StringEncoder(data), 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func newConsumer(brokerList []string) (sarama.Consumer, error) { 97 | config := sarama.NewConfig() 98 | 99 | config.Producer.RequiredAcks = sarama.WaitForLocal 100 | config.Producer.Compression = sarama.CompressionSnappy 101 | config.Producer.Flush.Frequency = 15 * time.Millisecond 102 | 103 | consumer, err := sarama.NewConsumer(brokerList, config) 104 | if err != nil { 105 | ErrorLogger.Printf("Failed to start sarama consumer: %v", err) 106 | return nil, fmt.Errorf("failed to start sarama consumer: %v", err) 107 | } 108 | 109 | return consumer, nil 110 | } 111 | 112 | func newProducer(brokerList []string) (sarama.AsyncProducer, error) { 113 | config := sarama.NewConfig() 114 | 115 | config.Producer.RequiredAcks = sarama.WaitForLocal // Only wait for the leader to ack 116 | config.Producer.Compression = sarama.CompressionSnappy // Compress messages 117 | config.Producer.Flush.Frequency = 15 * time.Millisecond // Flush batches every 500ms 118 | 119 | producer, err := sarama.NewAsyncProducer(brokerList, config) 120 | if err != nil { 121 | ErrorLogger.Printf("Failed to start sarama producer: %v", err) 122 | return nil, fmt.Errorf("failed to start sarama producer: %v", err) 123 | } 124 | 125 | go func() { 126 | for err := range producer.Errors() { 127 | ErrorLogger.Printf("Failed to write access log entry: %v", err) 128 | } 129 | }() 130 | 131 | return producer, nil 132 | } 133 | -------------------------------------------------------------------------------- /gosiris/zipkin.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import ( 4 | "github.com/opentracing/opentracing-go" 5 | zipkin "github.com/openzipkin/zipkin-go-opentracing" 6 | ) 7 | 8 | var ( 9 | zipkinSystemInitialized bool 10 | collector zipkin.Collector 11 | tracer opentracing.Tracer 12 | ) 13 | 14 | type ZipkinOptions struct { 15 | Url string 16 | Debug bool 17 | HostPort string 18 | SameSpan bool 19 | } 20 | 21 | func initZipkinSystem(actorSystemName string, options ZipkinOptions) error { 22 | c, err := zipkin.NewHTTPCollector(options.Url) 23 | 24 | collector = c 25 | 26 | if err != nil { 27 | ErrorLogger.Printf("Failed to create a Zipkin collector: %v", err) 28 | return err 29 | } 30 | 31 | recorder := zipkin.NewRecorder(collector, options.Debug, options.HostPort, actorSystemName) 32 | 33 | t, err := zipkin.NewTracer( 34 | recorder, 35 | zipkin.ClientServerSameSpan(options.SameSpan), 36 | zipkin.TraceID128Bit(true), 37 | ) 38 | 39 | tracer = t 40 | 41 | if err != nil { 42 | ErrorLogger.Printf("Failed to create a Zipkin tracer: %v", err) 43 | return err 44 | } 45 | 46 | opentracing.InitGlobalTracer(tracer) 47 | 48 | zipkinSystemInitialized = true 49 | 50 | InfoLogger.Printf("Zipkin tracer started") 51 | 52 | return nil 53 | } 54 | 55 | //func logZipkinFields(span opentracing.Span, fields ...zlog.Field) { 56 | // span.LogFields(fields...) 57 | //} 58 | 59 | func logZipkinMessage(span opentracing.Span, event string) { 60 | span.LogEvent(event) 61 | } 62 | 63 | func inject(span opentracing.Span) (opentracing.TextMapCarrier, error) { 64 | if span == nil { 65 | return nil, nil 66 | } 67 | 68 | carrier := opentracing.TextMapCarrier{} 69 | 70 | err := tracer.Inject(span.Context(), opentracing.TextMap, carrier) 71 | 72 | if err != nil { 73 | ErrorLogger.Printf("Failed to inject: %v", err) 74 | return nil, err 75 | } 76 | 77 | return carrier, nil 78 | } 79 | 80 | func extract(carrier opentracing.TextMapCarrier) (opentracing.SpanContext, error) { 81 | return tracer.Extract(opentracing.TextMap, carrier) 82 | } 83 | 84 | func startZipkinSpan(spanName, operationName string) opentracing.Span { 85 | if !zipkinSystemInitialized { 86 | ErrorLogger.Printf("Zipkin system not started") 87 | return nil 88 | } 89 | 90 | span := opentracing.StartSpan(spanName) 91 | span.SetOperationName(operationName) 92 | 93 | InfoLogger.Printf("Span %v started", spanName) 94 | 95 | return span 96 | } 97 | 98 | func stopZipkinSpan(span opentracing.Span) { 99 | span.Finish() 100 | } 101 | 102 | func closeZipkinSystem() { 103 | if zipkinSystemInitialized { 104 | InfoLogger.Printf("Closing Zipkin system") 105 | collector.Close() 106 | zipkinSystemInitialized = false 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /gosiris/zipkin_test.go: -------------------------------------------------------------------------------- 1 | package gosiris 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInit(t *testing.T) { 8 | //zipkinInit("http://192.168.99.100:9411/api/v1/spans", false, "0.0.0.0", "testx", true) 9 | //span.SetOperationName("toto") 10 | //span.LogEvent("testx") 11 | //span.LogEvent("testy") 12 | //span.Finish() 13 | //collector.Close() 14 | } 15 | --------------------------------------------------------------------------------