captainu-tincan 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/.travis.yml +22 -0
- data/Gemfile +4 -0
- data/Rakefile +10 -0
- data/bin/tincan +14 -0
- data/bin/tincanctl +91 -0
- data/lib/tincan.rb +8 -0
- data/lib/tincan/cli.rb +305 -0
- data/lib/tincan/failure.rb +67 -0
- data/lib/tincan/message.rb +79 -0
- data/lib/tincan/receiver.rb +167 -0
- data/lib/tincan/sender.rb +101 -0
- data/lib/tincan/version.rb +3 -0
- data/license.markdown +22 -0
- data/readme.markdown +120 -0
- data/spec/failure_spec.rb +58 -0
- data/spec/fixtures/failure.json +1 -0
- data/spec/fixtures/message.json +1 -0
- data/spec/message_spec.rb +68 -0
- data/spec/receiver_spec.rb +202 -0
- data/spec/sender_spec.rb +121 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/dummy.rb +8 -0
- data/spec/support/futuristic.rb +61 -0
- data/spec/tincan_spec.rb +7 -0
- data/tincan.gemspec +35 -0
- metadata +213 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'json'
|
3
|
+
require 'tincan/message'
|
4
|
+
|
5
|
+
module Tincan
|
6
|
+
# Encapsulates a failed attempt at a message attempted from a Redis queue.
|
7
|
+
class Failure
|
8
|
+
attr_accessor :failed_at, :attempt_count, :message_id, :queue_name
|
9
|
+
|
10
|
+
# Creates a new instance of a notification with an object (usually an
|
11
|
+
# ActiveModel instance).
|
12
|
+
# @param [Integer] message_id The identifier for the message to retry.
|
13
|
+
# @param [String] queue_name The name of the queue in which this failure
|
14
|
+
# originally occurred.
|
15
|
+
# @return [Tincan::Message] An instance of this class.
|
16
|
+
def initialize(message_id = nil, queue_name = nil)
|
17
|
+
self.message_id = message_id
|
18
|
+
self.attempt_count = 0
|
19
|
+
self.failed_at = DateTime.now
|
20
|
+
self.queue_name = queue_name
|
21
|
+
end
|
22
|
+
|
23
|
+
# Gives a date and time when this object is allowed to be attempted again,
|
24
|
+
# derived from when it last failed, plus the number of attempts, in
|
25
|
+
# seconds.
|
26
|
+
# @return [DateTime] The date/time when this is allowed to be retried.
|
27
|
+
def attempt_after
|
28
|
+
failed_at + Rational((attempt_count * 10), 86400)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Deserializes an object from a JSON string.
|
32
|
+
# @param [String] json A JSON string to be decoded.
|
33
|
+
# @return [Tincan::Failure] A deserialized failure.
|
34
|
+
def self.from_json(json)
|
35
|
+
from_hash(JSON.parse(json))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Assigns keys and values to this object based on an already-deserialized
|
39
|
+
# JSON object.
|
40
|
+
# @param [Hash] hash A hash of properties and their values.
|
41
|
+
# @return [Tincan::Failure] A failure.
|
42
|
+
def self.from_hash(hash)
|
43
|
+
instance = new(hash['message_id'], hash['queue_name'])
|
44
|
+
instance.attempt_count = hash['attempt_count'].to_i
|
45
|
+
instance
|
46
|
+
end
|
47
|
+
|
48
|
+
# Generates a version of this failure as a JSON string.
|
49
|
+
# @return [String] A JSON-compliant marshalling of this instance's data.
|
50
|
+
def to_json(options = {})
|
51
|
+
Hash[%i(failed_at attempt_count message_id queue_name).map do |name|
|
52
|
+
[name, send(name)]
|
53
|
+
end].to_json(options)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Object overrides
|
57
|
+
|
58
|
+
# Overrides equality operator to determine if all ivars are equal
|
59
|
+
def ==(other)
|
60
|
+
false unless other
|
61
|
+
checks = %i(failed_at attempt_count message_id queue_name).map do |p|
|
62
|
+
other.send(p) == send(p)
|
63
|
+
end
|
64
|
+
checks.all?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'json'
|
3
|
+
require 'active_support/inflector'
|
4
|
+
|
5
|
+
module Tincan
|
6
|
+
# Encapsulates a message published to (and received from) a Redis "tincan"
|
7
|
+
# queue.
|
8
|
+
class Message
|
9
|
+
attr_accessor :object_name, :change_type, :object_data, :published_at
|
10
|
+
|
11
|
+
# Creates a new instance of a notification with an object (usually an
|
12
|
+
# ActiveModel instance).
|
13
|
+
# @param [Block] Takes an optional block to configure this instance.
|
14
|
+
# @return [Tincan::Message] An instance of this class.
|
15
|
+
def initialize
|
16
|
+
yield self if block_given?
|
17
|
+
self.published_at ||= DateTime.now
|
18
|
+
end
|
19
|
+
|
20
|
+
# Deserializes an object from a JSON string.
|
21
|
+
# @param [String] json A JSON string to be decoded.
|
22
|
+
# @return [Tincan::Message] A deserialized notification.
|
23
|
+
def self.from_json(json)
|
24
|
+
from_hash(JSON.parse(json))
|
25
|
+
end
|
26
|
+
|
27
|
+
# Assigns keys and values to this object based on an already-deserialized
|
28
|
+
# JSON object.
|
29
|
+
# @param [Hash] hash A hash of properties and their values.
|
30
|
+
# @return [Tincan::Message] A message.
|
31
|
+
def self.from_hash(hash)
|
32
|
+
instance = new do |i|
|
33
|
+
hash.each do |key, val|
|
34
|
+
val = DateTime.iso8601(val) if key == 'published_at'
|
35
|
+
i.send("#{key}=".to_sym, val)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
instance
|
39
|
+
end
|
40
|
+
|
41
|
+
# Checks for proper change type and sets it if it's valid.
|
42
|
+
# @param [Symbol] value :create, :modify, or :delete.
|
43
|
+
def change_type=(value)
|
44
|
+
if %i(create modify delete).include?(value.to_sym)
|
45
|
+
@change_type = value.to_sym
|
46
|
+
else
|
47
|
+
fail ArgumentError, ':change_type must be :create, :modify or :delete'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Generates a version of this notification as a JSON string.
|
52
|
+
# @return [String] A JSON-compliant marshalling of this instance's data.
|
53
|
+
def to_json(options = {})
|
54
|
+
# Note: at some point I may want to override how this is done. I think
|
55
|
+
# Rabl could definitely be of some use here.
|
56
|
+
Hash[%i(object_name change_type object_data published_at).map do |name|
|
57
|
+
[name, send(name)]
|
58
|
+
end].to_json(options)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Object overrides
|
62
|
+
|
63
|
+
# Overrides equality operator to determine if all ivars are equal
|
64
|
+
def ==(other)
|
65
|
+
false unless other
|
66
|
+
checks = %i(object_name change_type object_data published_at).map do |p|
|
67
|
+
other.send(p) == send(p)
|
68
|
+
end
|
69
|
+
checks.all?
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
vars = instance_variables.map do |v|
|
74
|
+
"#{v}=#{instance_variable_get(v).inspect}"
|
75
|
+
end.join(', ')
|
76
|
+
"<#{self.class}: #{vars}>"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'tincan/failure'
|
2
|
+
require 'tincan/message'
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
module Tincan
|
6
|
+
# An object whose purpose is to listen to a variety of Redis queues and fire
|
7
|
+
# off notifications when triggered.
|
8
|
+
class Receiver
|
9
|
+
attr_reader :config
|
10
|
+
attr_accessor :client_name, :listen_to, :redis_host, :redis_port,
|
11
|
+
:namespace, :on_exception, :logger
|
12
|
+
|
13
|
+
# Lifecycle methods
|
14
|
+
|
15
|
+
# Creates and return a listener object, ready to listen. You can pass in
|
16
|
+
# either a hash or a block; the block takes priority.
|
17
|
+
# @param [Hash] options A list of keys/values to assign to this instance.
|
18
|
+
# @return [Tincan::Receiver] Self.
|
19
|
+
def initialize(options = {})
|
20
|
+
if block_given?
|
21
|
+
yield(self)
|
22
|
+
else
|
23
|
+
@config = options
|
24
|
+
ivars = %i(client_name listen_to redis_host redis_port namespace
|
25
|
+
on_exception logger)
|
26
|
+
ivars.each { |n| send("#{n}=".to_sym, @config[n]) }
|
27
|
+
end
|
28
|
+
self.redis_port ||= 6379
|
29
|
+
end
|
30
|
+
|
31
|
+
# Related objects
|
32
|
+
|
33
|
+
# The instance of a Redis communicator that can subscribe messages.
|
34
|
+
# @return [Redis] The Redis client used by this object.
|
35
|
+
def redis_client
|
36
|
+
@redis_client ||= ::Redis.new(host: redis_host, port: redis_port)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Transactional (submission) methods
|
40
|
+
|
41
|
+
# Registers this receiver against a Redis set based on the object name.
|
42
|
+
# Looks like "namespace:object_name:receivers".
|
43
|
+
# @return [Tincan::Receiver] Self.
|
44
|
+
def register
|
45
|
+
listen_to.keys.each do |object_name|
|
46
|
+
receiver_list_key = key_for_elements(object_name, 'receivers')
|
47
|
+
logger.info "Registered against Tincan set #{receiver_list_key}"
|
48
|
+
redis_client.sadd(receiver_list_key, client_name)
|
49
|
+
end
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
# Handles putting a message identifier into a failed "retries" list.
|
54
|
+
# @param [Integer] message_id The identifier of the failed message.
|
55
|
+
# @param [String] original_list The name of the originating list.
|
56
|
+
# @return [Integer] The number of failed entries in the same list.
|
57
|
+
def store_failed_message(message_id, original_list)
|
58
|
+
logger.warn "Storing failure #{message_id} for list #{original_list}"
|
59
|
+
failure = Failure.new(message_id, original_list)
|
60
|
+
store_failure(failure)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Handles putting a message identifier into a failed "retries" list.
|
64
|
+
# @param [Tincan::Failure] failure The failure to store.
|
65
|
+
# @return [Integer] The number of failed entries in the same list.
|
66
|
+
def store_failure(failure)
|
67
|
+
error_list = failure.queue_name.gsub('messages', 'failures')
|
68
|
+
redis_client.rpush(error_list, failure.to_json)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Message handling methods
|
72
|
+
|
73
|
+
# Iterates through stored lambdas for a given object, and passes the
|
74
|
+
# message to all of them.
|
75
|
+
# @param [String] object_name The object name gleamed from the list key.
|
76
|
+
# @param [Tincan::Message] message The Message generated from the JSON
|
77
|
+
# hash retrieved from Redis.
|
78
|
+
def handle_message_for_object(object_name, message)
|
79
|
+
logger.debug "Encountered #{object_name} message: #{message.object_data}"
|
80
|
+
listen_to[object_name.to_sym].each do |stored_lambda|
|
81
|
+
stored_lambda.call(message)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Loop methods
|
86
|
+
|
87
|
+
# Registers and subscribes. That is all.
|
88
|
+
def listen
|
89
|
+
register
|
90
|
+
subscribe
|
91
|
+
end
|
92
|
+
|
93
|
+
# Formatting and helper methods
|
94
|
+
|
95
|
+
# Asks the instance of Redis for the proper JSON data for a message, and
|
96
|
+
# then turns that into a Tincan::Message.
|
97
|
+
# @param [Integer] message_id The numerical ID of the message to retrieve.
|
98
|
+
# @param [String] object_name The object name of the message to retrieve;
|
99
|
+
# this helps the receiver determine which list it was
|
100
|
+
# posted to.
|
101
|
+
# @return [Tincan::Message] An initialized Message, or nil if not found.
|
102
|
+
def message_for_id(message_id, object_name)
|
103
|
+
key = key_for_elements(object_name, 'messages', message_id)
|
104
|
+
json = redis_client.get(key)
|
105
|
+
return nil unless json
|
106
|
+
Message.from_json(json)
|
107
|
+
end
|
108
|
+
|
109
|
+
# A flattened list of message list keys, in the format of
|
110
|
+
# "namespace:object_name:client:messages".
|
111
|
+
# @return [Array] An array of object_names formatted with client name, like
|
112
|
+
# "namespace:object_name:client:messages".
|
113
|
+
def message_list_keys
|
114
|
+
@message_list_keys ||= listen_to.keys.map do |object_name|
|
115
|
+
%w(messages failures).map do |type|
|
116
|
+
key_for_elements(object_name, client_name, type)
|
117
|
+
end
|
118
|
+
end.flatten
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# Loops on a blocking pop call to a series of message lists on Redis and
|
124
|
+
# yields a given block when triggered. Handles turning JSON back into
|
125
|
+
# notification messages. Uses `BLPOP` as a blocking pop, indefinitely,
|
126
|
+
# until we get a message.
|
127
|
+
def subscribe
|
128
|
+
logger.info 'Awaiting new messages from Tincan.'
|
129
|
+
loop do
|
130
|
+
begin
|
131
|
+
message_list, content = redis_client.blpop(message_list_keys)
|
132
|
+
object_name = message_list.split(':')[1]
|
133
|
+
message_type = message_list.split(':').last
|
134
|
+
|
135
|
+
if message_type == 'messages'
|
136
|
+
message = message_for_id(content, object_name)
|
137
|
+
elsif message_type == 'failures'
|
138
|
+
failure = Failure.from_json(content)
|
139
|
+
if failure.attempt_after > DateTime.now
|
140
|
+
store_failure(failure)
|
141
|
+
next
|
142
|
+
end
|
143
|
+
message = message_for_id(failure.message_id, object_name)
|
144
|
+
end
|
145
|
+
|
146
|
+
handle_message_for_object(object_name, message) if message
|
147
|
+
rescue Interrupt
|
148
|
+
logger.warn 'Encountered interrupt.'
|
149
|
+
raise
|
150
|
+
rescue Exception => e
|
151
|
+
logger.warn "Encountered exception #{e}."
|
152
|
+
on_exception.call(e, {}) if on_exception
|
153
|
+
next unless content
|
154
|
+
failure ||= Failure.new(content, message_list)
|
155
|
+
failure.attempt_count += 1
|
156
|
+
store_failure(failure)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Joins a variadic set of elements along with a namespace with colons.
|
162
|
+
# @return [String] A joined string to be used as a Redis key.
|
163
|
+
def key_for_elements(*elements)
|
164
|
+
([namespace] + elements).join(':')
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'tincan/message'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
module Tincan
|
5
|
+
# An object whose purpose is to send messages to a given series of Redis
|
6
|
+
# message queues for those receiving them.
|
7
|
+
class Sender
|
8
|
+
attr_reader :config
|
9
|
+
attr_accessor :redis_host, :redis_port, :namespace
|
10
|
+
|
11
|
+
# Lifecycle methods
|
12
|
+
|
13
|
+
# Creates and return a sender object, ready to send. You can pass in
|
14
|
+
# either a hash or a block; the block takes priority.
|
15
|
+
# @param [Hash] options A list of keys/values to assign to this instance.
|
16
|
+
# @return [Tincan::Receiver] Self.
|
17
|
+
def initialize(options = {})
|
18
|
+
if block_given?
|
19
|
+
yield(self)
|
20
|
+
else
|
21
|
+
@config = options
|
22
|
+
ivars = %i(redis_host redis_port namespace)
|
23
|
+
ivars.each { |n| send("#{n}=".to_sym, @config[n]) }
|
24
|
+
end
|
25
|
+
self.redis_port ||= 6379
|
26
|
+
end
|
27
|
+
|
28
|
+
# Related objects
|
29
|
+
|
30
|
+
# The instance of a Redis communicator that can publish messages.
|
31
|
+
# @return [Redis] The Redis client used by this object.
|
32
|
+
def redis_client
|
33
|
+
@redis_client ||= ::Redis.new(host: redis_host, port: redis_port)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Transactional (lookup) methods
|
37
|
+
|
38
|
+
# Asks Redis for the set of all active receivers and generates string
|
39
|
+
# keys for all of them. Formatted like "namespace:object:client:messages".
|
40
|
+
# @return [Array] An array of keys identifying all receiver pointer lists.
|
41
|
+
def keys_for_receivers(object_name)
|
42
|
+
receiver_list_key = key_for_elements(object_name, 'receivers')
|
43
|
+
receivers = redis_client.smembers(receiver_list_key)
|
44
|
+
receivers.map do |receiver|
|
45
|
+
key_for_elements(object_name, receiver, 'messages')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Tells Redis to delete any pending messages for registered receivers, by
|
50
|
+
# essentially deleting the message key.
|
51
|
+
# @return [Boolean] True, just because.
|
52
|
+
def flush_all_queues_for_object(object_name)
|
53
|
+
keys_for_receivers(object_name).each do |key|
|
54
|
+
redis_client.del(key)
|
55
|
+
end
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
# Communication methods
|
60
|
+
|
61
|
+
# Bundles up an object in a message object and publishes it to the Redis
|
62
|
+
# host.
|
63
|
+
# @param [Tincan::Message] message The message to publish.
|
64
|
+
# @param [Symbol] change_type :create, :modify, or :delete.
|
65
|
+
# @return [Boolean] true if the operation was a success.
|
66
|
+
def publish(message)
|
67
|
+
identifier = identifier_for_message(message)
|
68
|
+
redis_client.set(primary_key_for_message(message), message.to_json)
|
69
|
+
keys_for_receivers(message.object_name.downcase).each do |key|
|
70
|
+
redis_client.rpush(key, identifier)
|
71
|
+
end
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
# Formatting and helper methods
|
76
|
+
|
77
|
+
# Generates an identifier to be used for a message. It's unique!
|
78
|
+
# @param [Tincan::Message] message The message for which to generate a
|
79
|
+
# unique identifier.
|
80
|
+
# @return [Integer] A unique identifier number for the message.
|
81
|
+
def identifier_for_message(message)
|
82
|
+
message.published_at.to_time.to_i
|
83
|
+
end
|
84
|
+
|
85
|
+
# Generates a key to be used as the primary destination key in Redis.
|
86
|
+
# @param [Tincan::Message] message The message to use in key generation.
|
87
|
+
# @return [String] A properly-formatted key to be used with Redis.
|
88
|
+
def primary_key_for_message(message)
|
89
|
+
identifier = identifier_for_message(message)
|
90
|
+
key_for_elements(message.object_name.downcase, 'messages', identifier)
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Joins a variadic set of elements along with a namespace with colons.
|
96
|
+
# @return [String] A joined string to be used as a Redis key.
|
97
|
+
def key_for_elements(*elements)
|
98
|
+
([namespace] + elements).join(':')
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/license.markdown
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 CaptainU, LLC.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/readme.markdown
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
# Tincan
|
2
|
+
|
3
|
+
[](https://travis-ci.org/captainu/tincan) [](https://codeclimate.com/github/captainu/tincan) [](https://codeclimate.com/github/captainu/tincan)
|
4
|
+
|
5
|
+
Provides an easy way to register senders and receivers on a reliable Redis message queue, to be used in lieu of Redis's own pub/sub commands (which are connection-reliant). This uses Redis's lists and sets using a defined and namespaced series of keys that allows for a *sender* to publish structured notification messages to a Redis server, which then get referenced in multiple receiver-specific lists, all of which are being watched by a client running a blocking pop (Redis's `BLPOP` command). These clients, known as *receivers*, handle the messages and route them to any number of custom-defined Ruby lambdas.
|
6
|
+
|
7
|
+
This is a Ruby implementation of [David Marquis's outstanding post](http://davidmarquis.wordpress.com/2013/01/03/reliable-delivery-message-queues-with-redis/) about reliable delivery message queues with Redis. It borrows heavily from the amazing [Sidekiq](https://github.com/mperham/sidekiq)'s design regarding its command-line and deployment interface.
|
8
|
+
|
9
|
+
See below for some usage examples (more coming soon).
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
``` ruby
|
16
|
+
gem 'captainu-tincan', github: 'captainu/tincan', require: 'tincan'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
``` bash
|
22
|
+
$ bundle install
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
### Sender
|
28
|
+
|
29
|
+
``` ruby
|
30
|
+
sender = Tincan::Sender.new do |config|
|
31
|
+
config.redis_host = 'localhost'
|
32
|
+
config.namespace = 'tincan' # Or whatever you'd like
|
33
|
+
end
|
34
|
+
|
35
|
+
# some_object here is something that responds to #to_json
|
36
|
+
sender.publish(some_object, :create)
|
37
|
+
```
|
38
|
+
|
39
|
+
In order to reset a list of messages for all listening receivers, initiate a sender (like demonstrated above), and perform the following (where `some_object` is the singlar, underscored-and-downcased version of the name of the Class for which to nuke messages).
|
40
|
+
|
41
|
+
``` ruby
|
42
|
+
sender.flush_all_queues_for_object('some_object')
|
43
|
+
```
|
44
|
+
|
45
|
+
### Receiver
|
46
|
+
|
47
|
+
``` ruby
|
48
|
+
receiver = Tincan::Receiver.new do |config|
|
49
|
+
config.redis_host = 'localhost'
|
50
|
+
config.client_name = 'my-receiver'
|
51
|
+
config.namespace = 'tincan' # Same as above.
|
52
|
+
config.logger = ::Logger.new(STDOUT)
|
53
|
+
config.listen_to = {
|
54
|
+
college: [
|
55
|
+
->(data) { SomeThing.handle_data(data) },
|
56
|
+
->(data) { SomeOtherThing.handle_same_data(data) }
|
57
|
+
],
|
58
|
+
college_team: [
|
59
|
+
-> (data) { AnotherThing.handle_this_data(data) }
|
60
|
+
]
|
61
|
+
}
|
62
|
+
config.on_exception = lambda do |ex, context|
|
63
|
+
Airbrake.notify_or_ignore(ex, parameters: context)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# This call blocks and loops
|
68
|
+
receiver.listen
|
69
|
+
```
|
70
|
+
|
71
|
+
#### Rails integration for Receiver
|
72
|
+
|
73
|
+
Drop a `tincan.yml` file in your Rails project at `config/tincan.yml`, and make it look like this!
|
74
|
+
|
75
|
+
``` yaml
|
76
|
+
defaults: &defaults
|
77
|
+
redis_host: localhost
|
78
|
+
listen_to:
|
79
|
+
college:
|
80
|
+
- SomeModel.update_from_tincan
|
81
|
+
college_team:
|
82
|
+
- SomeOtherModel.update_from_tincan
|
83
|
+
|
84
|
+
development:
|
85
|
+
<<: *defaults
|
86
|
+
namespace: tincan-development
|
87
|
+
client_name: your_app-dev
|
88
|
+
|
89
|
+
test:
|
90
|
+
<<: *defaults
|
91
|
+
namespace: tincan-test
|
92
|
+
client_name: your_app-test
|
93
|
+
|
94
|
+
production:
|
95
|
+
<<: *defaults
|
96
|
+
namespace: tincan
|
97
|
+
client_name: your_app
|
98
|
+
```
|
99
|
+
|
100
|
+
Then, a command-line `tincan` worker is just a few keystrokes away.
|
101
|
+
|
102
|
+
``` bash
|
103
|
+
$ bundle exec tincan
|
104
|
+
```
|
105
|
+
|
106
|
+
You can even daemonize it with `-d`. The command-line library is largely modeled after [Sidekiq](https://github.com/mperham/sidekiq), and is currently in dire need of some tests. Use at your own risk.
|
107
|
+
|
108
|
+
For integration with Capistrano, see [capistrano-tincan](https://github.com/captainu/capistrano-tincan).
|
109
|
+
|
110
|
+
## Contributing
|
111
|
+
|
112
|
+
1. [Fork it](https://github.com/captainu/tincan/fork)!
|
113
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
114
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
115
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
116
|
+
5. Create a new pull request
|
117
|
+
|
118
|
+
## Contributors
|
119
|
+
|
120
|
+
- [Ben Kreeger](https://github.com/kreeger)
|