rocketman 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7add76c53ecb1c5996cc2c21e0141bcadeb4169d40bb9785c577286839eff7a7
4
- data.tar.gz: e2b0ff4413172768ef6a331b76fa75113fe80cbb1aa63a9fc8e62bba915b4b8e
3
+ metadata.gz: 52b5a1cb157137e4894f21237379ea6bfa577c1029cf3f6cfc435eb18b19c767
4
+ data.tar.gz: 1d47bc328a3ff19de8c15acd34ada270343d626335406878c967b4b734e3f213
5
5
  SHA512:
6
- metadata.gz: da1cd6a839cb80ae22eebb8ca1150bfacd555999a4ada7c348838647db7118d5e978641023459dce4c8ef2158d70043d84185d2e876153e2cea444b69616f266
7
- data.tar.gz: 37b70df7e25868cfe7d3de36b38d49c116e75a699b7a6ee680491ab4c428f212f24dab728f570e1266228483daed1074d15876a37432a8a22c2ef0ef80939ee6
6
+ metadata.gz: 26f006bd824c1993463984c6b57401fb866ad2293585c72b01506000e6af18f9358d90cd9b8558f17796820f7d20d4aa0da145cdcdd490e358d396788d187a32
7
+ data.tar.gz: 05ee47a76e61a3d6700e28a1208d80e856731d160330c373fa86cd4d38a471b8f7bcba481d9a55924dcc5ddd7171ccc311a355426919ec3763c2ff33cd18fd2f
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rocketman (0.1.1)
4
+ rocketman (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -13,6 +13,7 @@ GEM
13
13
  coderay (~> 1.1.0)
14
14
  method_source (~> 0.9.0)
15
15
  rake (10.5.0)
16
+ redis (4.1.2)
16
17
  rspec (3.8.0)
17
18
  rspec-core (~> 3.8.0)
18
19
  rspec-expectations (~> 3.8.0)
@@ -34,6 +35,7 @@ DEPENDENCIES
34
35
  bundler (~> 1.17)
35
36
  pry
36
37
  rake (~> 10.0)
38
+ redis
37
39
  rocketman!
38
40
  rspec (~> 3.0)
39
41
 
data/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  <sub>*yes, I know it says Starman on the image*</sub>
4
4
  > *🎶 And I think it's gonna be a long long time 'Till touch down brings me round again to find 🎶*
5
5
 
6
- Rocketman is a gem that introduces Pub-Sub mechanism within Ruby code.
6
+ Rocketman is a gem that introduces Pub-Sub mechanism within your Ruby code.
7
+
8
+ The main goal of Rocketman is not to replace proper message buses like Redis PubSub/Kafka, but rather be a stepping stone. You can read more about the [rationale behind the project](https://github.com/edisonywh/rocketman#why-use-rocketman-rather-than-a-proper-message-bus-eg-redis-pubsubkafka) down below.
7
9
 
8
10
  As with all Pub-Sub mechanism, this greatly decouples your upstream producer and downstream consumer, allowing for scalability, and easier refactor when you decide to move Pub-Sub to a separate service.
9
11
 
@@ -29,20 +31,84 @@ Or install it yourself as:
29
31
 
30
32
  Rocketman exposes two module, `Rocketman::Producer` and `Rocketman::Consumer`. They do exactly as what their name implies. All you need to do is `include Rocketman::Producer` and `extend Rocketman::Consumer` into your code.
31
33
 
32
- #### Producer
34
+ ### Producer
33
35
  Producer exposes one **instance** method to you: `:emit`. `:emit` takes in the event name and an optional payload and publishes it to the consumers. There's nothing more you need to do. The producer do not have to know who its consumers are.
34
36
 
35
37
  ```ruby
36
38
  class Producer
37
- include Rocketman::Producer
39
+ include Rocketman::Producer
40
+
41
+ def hello_world
42
+ emit :hello, payload: {"one" => 1, "two" => 2}
43
+ end
44
+ end
45
+ ```
46
+
47
+ Note that Producer emit events with threads that run in a thread pool. The default number of worker is 5, and the workers default to checking the job with a 3 seconds interval. You can tweak these to your liking, refer to the [`Configuration` section](https://github.com/edisonywh/rocketman#configuration) below for more informations.
48
+
49
+ ### Consumer
50
+ Consumer exposes a **class** method, `:on_event`. `:on_event` takes in the event name, and also an additional block, which gets executed whenever a message is received. If an additional `payload` is emitted along with the event, you can get access to it in the form of block argument.
51
+
52
+ ```ruby
53
+ class Consumer
54
+ extend Rocketman::Consumer
38
55
 
39
- def hello_world
40
- emit :hello, payload: {"one" => 1, "two" => 2}
41
- end
56
+ on_event :hello do |payload|
57
+ puts "I've received #{payload} here!"
58
+ # => I've received {:payload=>{"one"=>1, "two"=>2}} here!
59
+ end
42
60
  end
43
61
  ```
44
62
 
45
- Note that Producer emit events with threads that run in a thread pool. The default number of worker is 5, and the workers default to checking the job with a 3 seconds interval. You can tweak it to your liking like so:
63
+ Simple isn't it?
64
+
65
+ #### Consume events from external services
66
+
67
+ If you want to also consume events from external services, you're in luck (well, as long as you're using `Redis` anyway..)
68
+
69
+ Rocketman exposes a `Rocketman::Bridge`, which allows your Ruby code to start consuming events from Redis, **without any changes to your consumers**.
70
+
71
+ This works because `Bridge` will listen for events from those services on behalf of you, and then it'll push those events onto the internal `Registry`.
72
+
73
+ **This pattern is powerful because this means your consumers do not have to know where the events are coming from, as long as they're registed onto `Registry`.**
74
+
75
+ Right now, only `Redis` is supported. Assuming you have the `redis` gem installed, this is how you register a bridge.
76
+
77
+ ```ruby
78
+ Rocketman::Bridge.construct(Redis.new)
79
+ ```
80
+
81
+ That's all! Rocketman will translate the following
82
+
83
+ ```
84
+ redis-cli> PUBLISH hello payload
85
+ ```
86
+
87
+ to something understandable by your consumer, so a consumer only has to do:
88
+
89
+ ```ruby
90
+ on_event :hello do |payload|
91
+ puts payload
92
+ end
93
+ ```
94
+
95
+ Notice how it behaves exactly the same as if the events did not come from Redis :)
96
+
97
+ **NOTE**: You should always pass in a **new, dedicated** connection to `Redis` to `Bridge#construct`. This is because `redis.subscribe` will hog the whole Redis connection (not just Ruby process), so `Bridge` expects a dedicated connection for itself.
98
+
99
+ ## Persisting emitted events
100
+
101
+ By default, the events emitted from your app will be stored in an in-memory `Queue`, which will get processed by Rocketman threaded workers.
102
+
103
+ However this also means that if your app dies with events still in your job queue, your emitted events which are stored in-memory will be lost.
104
+
105
+ That is obviously not desirable, so that's why **Rocketman ships with an option to use `Redis` as your backing storage mechanism.**
106
+
107
+ All you need to do is pass in a `Redis` connection to Rocketman. Refer to the [`Configuration` section below](https://github.com/edisonywh/rocketman#configuration) for more information.
108
+
109
+ ## Configuration
110
+
111
+ Here are the available options to tweak for Rocketman.
46
112
 
47
113
  ```ruby
48
114
  # config/initializers/rocketman.rb
@@ -50,32 +116,32 @@ Note that Producer emit events with threads that run in a thread pool. The defau
50
116
  Rocketman.configure do |config|
51
117
  config.worker_count = 10 # defaults to 5
52
118
  config.latency = 1 # defaults to 3, unit is :seconds
119
+ config.storage = Redis.new # defaults to `nil`
120
+ config.debug = true # defaults to `false`
53
121
  end
54
122
  ```
55
123
 
56
- #### Consumer
57
- Consumer exposes a **class** method, `:on_event`. `:on_event` takes in the event name, and also an additional block, which gets executed whenever a message is received. If an additional `payload` is emitted along with the event, you can get access to it in the form of block argument.
124
+ Currently `storage` only supports `Redis`, suggestions for alternative backing mechanisms are welcomed.
58
125
 
59
- ```ruby
60
- class Consumer
61
- extend Rocketman::Consumer
126
+ `debug` mode enables some debugging `puts` statements, and also tweak the `Thread` workers to `abort_on_exception = true`. So if you have failing jobs, this is how you can figure out what's happening inside your workers.
62
127
 
63
- on_event :hello do |payload|
64
- puts "I've received #{payload} here!"
65
- # => I've received {:payload=>{"one"=>1, "two"=>2}} here!
66
- end
67
- end
68
- ```
128
+ ## Why use `Rocketman`, rather than a proper message bus (e.g Redis PubSub/Kafka)?
69
129
 
70
- Simple isn't it?
130
+ It is worth noting that `Rocketman` is not meant to be a replacement for the aforementioned projects -- both Redis PubSub and Kafka are battle-tested and I highly encourage to use them if you can.
131
+
132
+ **But**, `Rocketman` recognizes that it's not an easy task to spin up an external message bus to support event-driven architecture, and that's what it's trying to do - to be a stepping stone for eventual greatness.
133
+
134
+ Moving onto a event-driven architecture is not an easy task - your team has to agree on a message bus, the DevOps team needs the capacity to manage the message bus, and then what about clustering? failovers?
135
+
136
+ So what Rocketman offers you is that you can start writing your dream-state event-driven code **today**, and when the time comes and your team has the capacity to move to a different message bus, then it should be a minimal change.
71
137
 
72
138
  ## Roadmap
73
139
 
74
140
  Right now events are using a `fire-and-forget` mechanism, which is designed to not cause issue to producers. However, this also means that if a consumer fail to consume an event, it'll be lost forever. **Next thing on the roadmap is look into a retry strategy + persistence mechanism.**
75
141
 
76
- Events are also stored in memory in `Rocketman::Registry`, which has the same problem as above. **Something to think about is to perhaps move it onto a persistent storage, like Redis for example.**
142
+ ~~Emitted events are also stored in memory in `Rocketman::Pool`, which means that there's a chance that you'll lose all emitted jobs. Something to think about is to perhaps move the emitted events/job queue onto a persistent storage, like Redis for example.~~ **Redis support is now available!**
77
143
 
78
- The interface could also probably be better defined, as one of the goal of Rocketman is to be the stepping stone before migrating off to a real, proper message queue/pub-sub mechanism like Kafka. **I want to revisit and think about how can we make that transition more seamless?**
144
+ The interface could also probably be better defined, as one of the goal of Rocketman is to be the stepping stone before migrating off to a real, proper message queue/pub-sub mechanism like Kafka. **I want to revisit and think about how can we make that transition more seamless.**
79
145
 
80
146
  ## Development
81
147
 
@@ -4,3 +4,4 @@ require 'rocketman/registry.rb'
4
4
  require 'rocketman/event.rb'
5
5
  require 'rocketman/producer.rb'
6
6
  require 'rocketman/consumer.rb'
7
+ require 'rocketman/bridge.rb'
@@ -0,0 +1,29 @@
1
+ module Rocketman
2
+ class Bridge
3
+ include Rocketman::Producer
4
+
5
+ attr_reader :service
6
+
7
+ def initialize(service)
8
+ @service = service
9
+ end
10
+
11
+ def self.construct(service)
12
+ instance = new(service)
13
+
14
+ case instance.service.class.to_s
15
+ when "Redis"
16
+ puts "Rocketman> Using Redis as external producer".freeze
17
+ Thread.new do
18
+ instance.service.psubscribe("*") do |on|
19
+ on.pmessage do |_pattern, event, payload|
20
+ instance.emit(event, payload)
21
+ end
22
+ end
23
+ end
24
+ else
25
+ puts "Rocketman> Don't know how to handle service: `#{service}`"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -8,11 +8,13 @@ module Rocketman
8
8
  end
9
9
 
10
10
  class Configuration
11
- attr_accessor :worker_count, :latency
11
+ attr_accessor :worker_count, :latency, :storage, :debug
12
12
 
13
13
  def initialize
14
14
  @worker_count = 5
15
15
  @latency = 3
16
+ @storage= nil
17
+ @debug = false
16
18
  end
17
19
  end
18
20
  end
@@ -2,14 +2,8 @@ module Rocketman
2
2
  module Consumer
3
3
  def on_event(event, &action)
4
4
  consumer = self
5
- Rocketman::Registry.instance.register_event(event)
6
- register_consumer(event, consumer, action)
7
- end
8
-
9
- private
10
-
11
- def register_consumer(event, consumer, action)
12
- Rocketman::Registry.instance.register_consumer(event, consumer, action)
5
+ Rocketman::Registry.register_event(event)
6
+ Rocketman::Registry.register_consumer(event, consumer, action)
13
7
  end
14
8
  end
15
9
  end
@@ -3,12 +3,13 @@ module Rocketman
3
3
  def initialize(event, payload)
4
4
  @event = event
5
5
  @payload = payload
6
- @test = payload.fetch(:test, false)
7
- Rocketman::Registry.instance.register_event(event)
6
+ Rocketman::Registry.register_event(event)
8
7
  end
9
8
 
10
9
  def notify_consumers
11
- consumers = Rocketman::Registry.instance.get_consumers_for(@event)
10
+ consumers = Rocketman::Registry.get_consumers_for(@event)
11
+
12
+ return if consumers.nil? || consumers.empty?
12
13
 
13
14
  consumers.each do |consumer, action|
14
15
  consumer.instance_exec(@payload, &action)
@@ -0,0 +1,76 @@
1
+ require 'forwardable'
2
+ require 'json'
3
+
4
+ module Rocketman
5
+ class JobQueue
6
+ extend Forwardable
7
+
8
+ QUEUE_KEY = "rocketman".freeze
9
+
10
+ def_delegators :@jobs, :<<, :empty?, :size, :clear, :push, :pop
11
+
12
+ def initialize
13
+ @storage = Rocketman.configuration.storage
14
+ @jobs = get_job_queue
15
+
16
+ at_exit { persist_events } if @storage.class.to_s == "Redis"
17
+ end
18
+
19
+ def schedule(job)
20
+ @jobs << job
21
+ end
22
+
23
+ private
24
+
25
+ def get_job_queue
26
+ case @storage.class.to_s
27
+ when "Redis"
28
+ rehydrate_events
29
+ else
30
+ Queue.new
31
+ end
32
+ end
33
+
34
+ def rehydrate_events
35
+ queue = Queue.new
36
+
37
+ if raw_data = @storage.get(QUEUE_KEY)
38
+ puts "Rehydrating Rocketman events from #{@storage.class}" if Rocketman.configuration.debug
39
+
40
+ rehydrate = JSON.restore(raw_data) # For security measure to prevent remote code execution (only allow contents valid in JSON)
41
+ jobs = Marshal.load(rehydrate)
42
+ event_count = 0
43
+
44
+ until jobs.empty?
45
+ queue << jobs.shift
46
+ event_count += 1
47
+ end
48
+
49
+ puts "Rehydrated #{event_count} events from #{@storage.class}" if Rocketman.configuration.debug
50
+
51
+ @storage.del(QUEUE_KEY) # After rehydration, delete it off Redis
52
+ end
53
+
54
+ queue
55
+ end
56
+
57
+ def persist_events
58
+ return if @jobs.empty?
59
+
60
+ puts "Persisting Rocketman events to #{@storage.class}" if Rocketman.configuration.debug
61
+ intermediary = []
62
+ event_count = 0
63
+
64
+ until @jobs.empty?
65
+ intermediary << @jobs.pop
66
+ event_count += 1
67
+ end
68
+ @jobs.close
69
+
70
+ marshalled_json = Marshal.dump(intermediary).to_json # For security measure to prevent remote code execution (only allow contents valid in JSON)
71
+
72
+ @storage.set(QUEUE_KEY, marshalled_json)
73
+ puts "Persisted #{event_count} events to #{@storage.class}" if Rocketman.configuration.debug
74
+ end
75
+ end
76
+ end
@@ -1,15 +1,18 @@
1
1
  require 'singleton'
2
+ require 'rocketman/job_queue'
2
3
 
3
4
  module Rocketman
4
5
  class Pool
5
6
  include Singleton
6
7
 
8
+ attr_reader :jobs
9
+
7
10
  def initialize
8
11
  worker_count = Rocketman.configuration.worker_count
9
12
  latency = Rocketman.configuration.latency
10
13
 
11
14
  @latency = latency
12
- @jobs = Queue.new
15
+ @jobs = Rocketman::JobQueue.new
13
16
  @workers = []
14
17
 
15
18
  worker_count.times do
@@ -19,17 +22,15 @@ module Rocketman
19
22
  # spawn_supervisor # TODO: Write a supervisor to monitor workers health, and restart if necessary
20
23
  end
21
24
 
22
- def schedule(&job)
23
- @jobs << job
24
- end
25
-
26
25
  private
27
26
 
28
27
  def spawn_worker
28
+ Thread.abort_on_exception = true if Rocketman.configuration.debug
29
+
29
30
  Thread.new do
30
31
  loop do
31
32
  job = @jobs.pop
32
- job.call
33
+ job.notify_consumers # Job is an instance of Rocketman::Event
33
34
  sleep @latency
34
35
  end
35
36
  end
@@ -1,8 +1,8 @@
1
1
  module Rocketman
2
2
  module Producer
3
- def emit(event, **payload)
4
- event = Rocketman::Event.new(event, payload)
5
- Rocketman::Pool.instance.schedule { event.notify_consumers }
3
+ def emit(event, payload = {})
4
+ event = Rocketman::Event.new(event.to_sym, payload)
5
+ Rocketman::Pool.instance.jobs.schedule(event)
6
6
  end
7
7
  end
8
8
  end
@@ -1,4 +1,5 @@
1
1
  require 'singleton'
2
+ require 'forwardable'
2
3
 
3
4
  module Rocketman
4
5
  class Registry
@@ -20,6 +21,10 @@ module Rocketman
20
21
  @registry[event][consumer] = action
21
22
  end
22
23
 
24
+ def get_events
25
+ @registry.keys
26
+ end
27
+
23
28
  def get_consumers_for(event)
24
29
  @registry[event]
25
30
  end
@@ -27,5 +32,11 @@ module Rocketman
27
32
  def event_exists?(event)
28
33
  !@registry[event].nil?
29
34
  end
35
+
36
+ # This is to help hide the Singleton interface from the rest of the code
37
+ class << self
38
+ extend Forwardable
39
+ def_delegators :instance, *Rocketman::Registry.instance_methods(false)
40
+ end
30
41
  end
31
42
  end
@@ -1,3 +1,3 @@
1
1
  module Rocketman
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.require_paths = ["lib"]
24
24
 
25
25
  spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "redis"
26
27
  spec.add_development_dependency "bundler", "~> 1.17"
27
28
  spec.add_development_dependency "rake", "~> 10.0"
28
29
  spec.add_development_dependency "rspec", "~> 3.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rocketman
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Edison Yap
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-08-11 00:00:00.000000000 Z
11
+ date: 2019-08-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -85,9 +99,11 @@ files:
85
99
  - bin/console
86
100
  - bin/setup
87
101
  - lib/rocketman.rb
102
+ - lib/rocketman/bridge.rb
88
103
  - lib/rocketman/config.rb
89
104
  - lib/rocketman/consumer.rb
90
105
  - lib/rocketman/event.rb
106
+ - lib/rocketman/job_queue.rb
91
107
  - lib/rocketman/pool.rb
92
108
  - lib/rocketman/producer.rb
93
109
  - lib/rocketman/registry.rb