magic_pipe 0.1.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 +11 -0
- data/.rspec +3 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +92 -0
- data/LICENSE.txt +19 -0
- data/README.md +241 -0
- data/Rakefile +6 -0
- data/bin/console +16 -0
- data/bin/setup +8 -0
- data/lib/magic_pipe.rb +45 -0
- data/lib/magic_pipe/client.rb +41 -0
- data/lib/magic_pipe/codecs.rb +24 -0
- data/lib/magic_pipe/codecs/base.rb +26 -0
- data/lib/magic_pipe/codecs/json.rb +38 -0
- data/lib/magic_pipe/codecs/message_pack.rb +81 -0
- data/lib/magic_pipe/codecs/thrift.rb +17 -0
- data/lib/magic_pipe/codecs/yaml.rb +18 -0
- data/lib/magic_pipe/config.rb +103 -0
- data/lib/magic_pipe/envelope.rb +29 -0
- data/lib/magic_pipe/errors.rb +14 -0
- data/lib/magic_pipe/loaders.rb +22 -0
- data/lib/magic_pipe/loaders/simple_active_record.rb +54 -0
- data/lib/magic_pipe/metrics.rb +54 -0
- data/lib/magic_pipe/senders.rb +23 -0
- data/lib/magic_pipe/senders/async.rb +76 -0
- data/lib/magic_pipe/senders/base.rb +24 -0
- data/lib/magic_pipe/senders/metrics_mixin.rb +20 -0
- data/lib/magic_pipe/senders/sync.rb +38 -0
- data/lib/magic_pipe/transports.rb +26 -0
- data/lib/magic_pipe/transports/base.rb +17 -0
- data/lib/magic_pipe/transports/debug.rb +17 -0
- data/lib/magic_pipe/transports/https.rb +80 -0
- data/lib/magic_pipe/transports/kafka.rb +8 -0
- data/lib/magic_pipe/transports/log.rb +15 -0
- data/lib/magic_pipe/transports/multi.rb +44 -0
- data/lib/magic_pipe/transports/sqs.rb +68 -0
- data/lib/magic_pipe/version.rb +3 -0
- data/magic_pipe.gemspec +44 -0
- metadata +257 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module MagicPipe
|
2
|
+
module Loaders
|
3
|
+
def self.lookup(type)
|
4
|
+
case type
|
5
|
+
when :simple_active_record then SimpleActiveRecord
|
6
|
+
when Class then type
|
7
|
+
else
|
8
|
+
raise ConfigurationError, "Unknown MagicPipe::Loaders type: '#{type}'."
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
files = File.expand_path("../loaders/**/*.rb", __FILE__)
|
15
|
+
Dir[files].each do |f|
|
16
|
+
begin
|
17
|
+
require f
|
18
|
+
rescue LoadError
|
19
|
+
# Some components have extra dependencies
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module MagicPipe
|
2
|
+
module Loaders
|
3
|
+
class SimpleActiveRecord
|
4
|
+
def initialize(record, wrapper=nil)
|
5
|
+
@record = record
|
6
|
+
@wrapper = wrapper
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :record
|
10
|
+
|
11
|
+
def decompose
|
12
|
+
{
|
13
|
+
klass: @record.class.to_s,
|
14
|
+
id: @record.id,
|
15
|
+
wrapper: (@wrapper && @wrapper.to_s),
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def load(decomposed_data)
|
22
|
+
input = symbolize_keys(decomposed_data)
|
23
|
+
record_klass_name = input[:klass]
|
24
|
+
record_id = input[:id]
|
25
|
+
wrapper_klass_name = input[:wrapper]
|
26
|
+
|
27
|
+
record_klass = load_constant!(record_klass_name, "ActiveRecord model")
|
28
|
+
record = record_klass.find(record_id) # let it raise ActiveRecord::RecordNotFound
|
29
|
+
|
30
|
+
if wrapper_klass_name
|
31
|
+
wrapper_klass = load_constant!(wrapper_klass_name, "object serializer")
|
32
|
+
wrapper_klass.new(record)
|
33
|
+
else
|
34
|
+
record
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def load_constant!(name, context)
|
41
|
+
Object.const_get(name)
|
42
|
+
rescue NameError
|
43
|
+
raise MagicPipe::LoaderError.new(name, context)
|
44
|
+
end
|
45
|
+
|
46
|
+
def symbolize_keys(hash)
|
47
|
+
hash.map do |k, v|
|
48
|
+
[k.to_sym, v]
|
49
|
+
end.to_h
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module MagicPipe
|
2
|
+
class Metrics
|
3
|
+
def initialize(config)
|
4
|
+
@client = config.metrics_client
|
5
|
+
@default_tags = build_default_tags(config)
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :client
|
9
|
+
|
10
|
+
def increment(metric, tags: [])
|
11
|
+
@client.increment(metric, tags: all_tags(tags))
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
|
18
|
+
def all_tags(list)
|
19
|
+
@default_tags + list
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_default_tags(config)
|
23
|
+
list = [
|
24
|
+
"producer:#{config.producer_name.to_s.gsub(" ", "_")}",
|
25
|
+
"pipe_instance:#{config.client_name.to_s}",
|
26
|
+
"loader:#{config.loader.to_s}",
|
27
|
+
"codec:#{config.codec.to_s}",
|
28
|
+
"transport:#{transport_tag(config)}",
|
29
|
+
"sender:#{config.sender.to_s}",
|
30
|
+
]
|
31
|
+
end
|
32
|
+
|
33
|
+
def transport_tag(config)
|
34
|
+
t = config.transport
|
35
|
+
if t.is_a?(Array)
|
36
|
+
"multi_" + t.map { |s| sanitize_tag_string(s) }.join("-")
|
37
|
+
else
|
38
|
+
t.to_s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def sanitize_tag_string(value)
|
43
|
+
value.to_s.tr(":. /", "")
|
44
|
+
end
|
45
|
+
|
46
|
+
def method_missing(name, *args, &block)
|
47
|
+
client.public_send(name, *args, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def respond_to_missing?(name, include_all)
|
51
|
+
client.respond_to?(name, include_all)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module MagicPipe
|
2
|
+
module Senders
|
3
|
+
def self.lookup(type)
|
4
|
+
case type
|
5
|
+
when :sync then Sync
|
6
|
+
when :async then Async
|
7
|
+
when Class then type
|
8
|
+
else
|
9
|
+
raise ConfigurationError, "Unknown MagicPipe::Senders type: '#{type}'."
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
files = File.expand_path("../senders/**/*.rb", __FILE__)
|
16
|
+
Dir[files].each do |f|
|
17
|
+
begin
|
18
|
+
require f
|
19
|
+
rescue LoadError
|
20
|
+
# Some components have extra dependencies
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
require "magic_pipe/senders/base"
|
3
|
+
require "magic_pipe/senders/metrics_mixin"
|
4
|
+
|
5
|
+
module MagicPipe
|
6
|
+
module Senders
|
7
|
+
class Async < Base
|
8
|
+
class Worker
|
9
|
+
include Sidekiq::Worker
|
10
|
+
include Senders::MetricsMixin
|
11
|
+
|
12
|
+
def perform(decomposed_object, topic, time, client_name)
|
13
|
+
client = MagicPipe.lookup_client(client_name)
|
14
|
+
object = client.loader.load(decomposed_object)
|
15
|
+
codec = client.codec
|
16
|
+
|
17
|
+
metadata = {
|
18
|
+
topic: topic,
|
19
|
+
producer: client.config.producer_name,
|
20
|
+
time: time.to_i,
|
21
|
+
mime: codec::TYPE
|
22
|
+
}
|
23
|
+
|
24
|
+
envelope = Envelope.new(
|
25
|
+
body: object,
|
26
|
+
**metadata
|
27
|
+
)
|
28
|
+
|
29
|
+
payload = codec.new(envelope).encode
|
30
|
+
client.transport.submit(payload, metadata)
|
31
|
+
|
32
|
+
track_success(client.metrics, topic)
|
33
|
+
rescue => e
|
34
|
+
track_failure(client.metrics, topic)
|
35
|
+
raise e
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
SETTINGS = {
|
41
|
+
"class" => Worker,
|
42
|
+
"retry" => true
|
43
|
+
}
|
44
|
+
|
45
|
+
def call
|
46
|
+
enqueue
|
47
|
+
end
|
48
|
+
|
49
|
+
def enqueue
|
50
|
+
options = SETTINGS.merge({
|
51
|
+
"queue" => queue_name,
|
52
|
+
"args" => [
|
53
|
+
decomposed_object,
|
54
|
+
@topic,
|
55
|
+
@time.to_i,
|
56
|
+
@config.client_name
|
57
|
+
]
|
58
|
+
})
|
59
|
+
Sidekiq::Client.push(options)
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
|
66
|
+
def queue_name
|
67
|
+
@config.async_transport_options[:queue]
|
68
|
+
end
|
69
|
+
|
70
|
+
def decomposed_object
|
71
|
+
loader = MagicPipe::Loaders.lookup(@config.loader)
|
72
|
+
loader.new(@object, @wrapper).decompose
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module MagicPipe
|
2
|
+
module Senders
|
3
|
+
class Base
|
4
|
+
# object should be something similar
|
5
|
+
# to an ActiveModel::Serializer or
|
6
|
+
# ActiveRecord object.
|
7
|
+
#
|
8
|
+
def initialize(object, topic, wrapper, time, codec, transport, config, metrics)
|
9
|
+
@object = object
|
10
|
+
@topic = topic
|
11
|
+
@wrapper = wrapper
|
12
|
+
@time = time
|
13
|
+
@codec = codec
|
14
|
+
@transport = transport
|
15
|
+
@config = config
|
16
|
+
@metrics = metrics
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module MagicPipe
|
2
|
+
module Senders
|
3
|
+
module MetricsMixin
|
4
|
+
def track_success(metrics, topic)
|
5
|
+
metrics.increment(
|
6
|
+
"magic_pipe.senders.mgs_sent",
|
7
|
+
tags: ["topic:#{topic}"]
|
8
|
+
)
|
9
|
+
end
|
10
|
+
|
11
|
+
def track_failure(metrics, topic)
|
12
|
+
metrics.increment(
|
13
|
+
"magic_pipe.senders.failure",
|
14
|
+
tags: ["topic:#{topic}"]
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "magic_pipe/senders/base"
|
2
|
+
require "magic_pipe/senders/metrics_mixin"
|
3
|
+
|
4
|
+
module MagicPipe
|
5
|
+
module Senders
|
6
|
+
class Sync < Base
|
7
|
+
include MetricsMixin
|
8
|
+
|
9
|
+
def call
|
10
|
+
metadata = build_metadata
|
11
|
+
envelope = build_message(metadata)
|
12
|
+
payload = @codec.new(envelope).encode
|
13
|
+
@transport.submit(payload, metadata)
|
14
|
+
track_success(@metrics, @topic)
|
15
|
+
rescue => e
|
16
|
+
track_failure(@metrics, @topic)
|
17
|
+
raise e
|
18
|
+
end
|
19
|
+
|
20
|
+
def build_message(metadata)
|
21
|
+
Envelope.new(body: data, **metadata)
|
22
|
+
end
|
23
|
+
|
24
|
+
def build_metadata
|
25
|
+
{
|
26
|
+
topic: @topic,
|
27
|
+
producer: @config.producer_name,
|
28
|
+
time: @time.to_i,
|
29
|
+
mime: @codec::TYPE
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def data
|
34
|
+
@wrapper ? @wrapper.new(@object) : @object
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module MagicPipe
|
2
|
+
module Transports
|
3
|
+
def self.lookup(type)
|
4
|
+
case type
|
5
|
+
when :https then Https
|
6
|
+
when :sqs then Sqs
|
7
|
+
when :log then Log
|
8
|
+
when :debug then Debug
|
9
|
+
when Array then Multi
|
10
|
+
when Class then type
|
11
|
+
else
|
12
|
+
raise ConfigurationError, "Unknown MagicPipe::Transports type: '#{type}'."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
files = File.expand_path("../transports/**/*.rb", __FILE__)
|
19
|
+
Dir[files].each do |f|
|
20
|
+
begin
|
21
|
+
require f
|
22
|
+
rescue LoadError
|
23
|
+
# Some components have extra dependencies
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module MagicPipe
|
2
|
+
module Transports
|
3
|
+
class Base
|
4
|
+
def initialize(config, metrics)
|
5
|
+
@config = config
|
6
|
+
@metrics = metrics
|
7
|
+
@logger = @config.logger
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :metrics, :logger
|
11
|
+
|
12
|
+
def submit(payload, metadata)
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "magic_pipe/transports/base"
|
2
|
+
|
3
|
+
module MagicPipe
|
4
|
+
module Transports
|
5
|
+
class Debug < Base
|
6
|
+
def initialize(*)
|
7
|
+
end
|
8
|
+
|
9
|
+
def submit(payload, metadata)
|
10
|
+
$magic_pipe_out = {
|
11
|
+
payload: payload,
|
12
|
+
metadata: metadata
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "magic_pipe/transports/base"
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
require "typhoeus"
|
5
|
+
require "typhoeus/adapters/faraday"
|
6
|
+
|
7
|
+
module MagicPipe
|
8
|
+
module Transports
|
9
|
+
class Https < Base
|
10
|
+
def initialize(config, metrics)
|
11
|
+
super(config, metrics)
|
12
|
+
@options = @config.https_transport_options
|
13
|
+
@conn = build_connection
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :conn
|
17
|
+
|
18
|
+
|
19
|
+
# TODO: should this raise an error on failure?
|
20
|
+
# So that it can be retried?
|
21
|
+
#
|
22
|
+
def submit(payload, metadata)
|
23
|
+
@conn.post do |r|
|
24
|
+
r.body = payload
|
25
|
+
r.headers["X-MagicPipe-Sent-At"] = metadata[:time]
|
26
|
+
r.headers["X-MagicPipe-Topic"] = metadata[:topic]
|
27
|
+
r.headers["X-MagicPipe-Producer"] = metadata[:producer]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def url
|
35
|
+
@options.fetch(:url)
|
36
|
+
end
|
37
|
+
|
38
|
+
def auth_token
|
39
|
+
@options.fetch(:auth_token)
|
40
|
+
end
|
41
|
+
|
42
|
+
def timeout
|
43
|
+
@options.fetch(:timeout)
|
44
|
+
end
|
45
|
+
|
46
|
+
def open_timeout
|
47
|
+
@options.fetch(:open_timeout)
|
48
|
+
end
|
49
|
+
|
50
|
+
def content_type
|
51
|
+
MagicPipe::Codecs.lookup(@config.codec)::TYPE
|
52
|
+
end
|
53
|
+
|
54
|
+
def user_agent
|
55
|
+
"MagicPipe v%s (Faraday v%s, Typhoeus v%s)" % [
|
56
|
+
MagicPipe::VERSION,
|
57
|
+
Faraday::VERSION,
|
58
|
+
Typhoeus::VERSION
|
59
|
+
]
|
60
|
+
end
|
61
|
+
|
62
|
+
# For a single backend, can't this be cached as a read only global?
|
63
|
+
#
|
64
|
+
def build_connection
|
65
|
+
Faraday.new(url) do |f|
|
66
|
+
f.request :retry, max: 2, interval: 0.1, backoff_factor: 2
|
67
|
+
f.request :basic_auth, auth_token, 'x'
|
68
|
+
|
69
|
+
f.headers['Content-Type'] = content_type
|
70
|
+
f.headers['User-Agent'] = user_agent
|
71
|
+
|
72
|
+
f.options.timeout = timeout
|
73
|
+
f.options.open_timeout = open_timeout
|
74
|
+
|
75
|
+
f.adapter :typhoeus
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|