captainu-tincan 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/captainu/tincan.svg?branch=master)](https://travis-ci.org/captainu/tincan) [![Code Climate](https://codeclimate.com/github/captainu/tincan.png)](https://codeclimate.com/github/captainu/tincan) [![Code Climate](https://codeclimate.com/github/captainu/tincan/coverage.png)](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)
|