magic_pipe 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|