marquise 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +2 -0
- data/Guardfile +12 -0
- data/README.md +110 -0
- data/Rakefile +39 -0
- data/lib/marquise.rb +212 -0
- data/lib/marquise/ffi.rb +65 -0
- data/marquise.gemspec +33 -0
- data/spec/marquise_spec.rb +274 -0
- data/spec/marquise_tell_binary_spec.rb +183 -0
- data/spec/marquise_tell_counter_spec.rb +153 -0
- data/spec/marquise_tell_int_spec.rb +172 -0
- data/spec/marquise_tell_real_spec.rb +161 -0
- data/spec/marquise_tell_spec.rb +27 -0
- data/spec/marquise_tell_text_spec.rb +153 -0
- data/spec/spec_helper.rb +25 -0
- metadata +259 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
guard 'spork', :rspec_env => { 'RACK_ENV' => 'test' } do
|
2
|
+
watch('Gemfile')
|
3
|
+
watch('spec/spec_helper.rb') { :rspec }
|
4
|
+
end
|
5
|
+
|
6
|
+
guard 'rspec',
|
7
|
+
:cmd => "rspec --drb",
|
8
|
+
:all_on_start => true do
|
9
|
+
watch('lib/marquise/ffi.rb')
|
10
|
+
watch(%r{^spec/.+_spec\.rb$})
|
11
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| Dir["spec/#{m[1].gsub('/', '_')}*_spec.rb"] }
|
12
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# Marquise for Ruby
|
2
|
+
|
3
|
+
This package contains Ruby bindings for the
|
4
|
+
[Marquise](https://github.com/anchor/libmarquise) data vault insertion
|
5
|
+
library. You should use it if you want to send data points to
|
6
|
+
[Vaultaire](https://github.com/anchor/vaultaire) in Ruby code.
|
7
|
+
|
8
|
+
|
9
|
+
# Installation
|
10
|
+
|
11
|
+
You should make sure you have libmarquise installed correctly first -- this
|
12
|
+
package will not work properly without it.
|
13
|
+
|
14
|
+
Then, simply install the `marquise` gem from Rubygems:
|
15
|
+
|
16
|
+
gem install marquise
|
17
|
+
|
18
|
+
And you should be good to go.
|
19
|
+
|
20
|
+
|
21
|
+
# Usage
|
22
|
+
|
23
|
+
Simply create a `Marquise` object, with the URL of the zeroMQ broker you
|
24
|
+
wish to use:
|
25
|
+
|
26
|
+
s = Marquise.new('tcp://chateau.example.com:5560')
|
27
|
+
|
28
|
+
When you're finished with your instance, you should close it, to cleanup
|
29
|
+
file handles and such:
|
30
|
+
|
31
|
+
s.close
|
32
|
+
|
33
|
+
If you're someone who doesn't like to stuff around with manual resource
|
34
|
+
release, there's also the very Rubyish block form:
|
35
|
+
|
36
|
+
Marquise.open('tcp://chateau.example.com:5560') do |s|
|
37
|
+
# Do things with s
|
38
|
+
end
|
39
|
+
|
40
|
+
Which will create a new Marquise object, yield it to your block, then close
|
41
|
+
off the thing when you're done.
|
42
|
+
|
43
|
+
|
44
|
+
# Sending data points
|
45
|
+
|
46
|
+
You send data points through a `Marquise` instance by `tell`ing it; these
|
47
|
+
are the simplest possible forms for each type of data Vaultaire can store:
|
48
|
+
|
49
|
+
s.tell(42) # Send an integer
|
50
|
+
s.tell(Math::PI) # Send a float
|
51
|
+
s.tell("Hello World") # Send a string
|
52
|
+
s.tell("Hello World".encode("ASCII-8BIT") # Send a binary blob
|
53
|
+
s.tell # Send a counter increment
|
54
|
+
|
55
|
+
|
56
|
+
## Types and type conversion
|
57
|
+
|
58
|
+
Vaultaire is able to store:
|
59
|
+
|
60
|
+
* UTF-8 Strings;
|
61
|
+
* Binary blobs;
|
62
|
+
* Integers, in the range -(2^63)+1 to (2^63)-1;
|
63
|
+
* Double-precision floating-point numbers;
|
64
|
+
* Counter increments.
|
65
|
+
|
66
|
+
Marquise will automatically determine the most appropriate type to store the
|
67
|
+
value in, given the type of the first argument passed to `#tell`, by
|
68
|
+
applying the following rules:
|
69
|
+
|
70
|
+
1. If the first argument `is_a? Hash`, then store a counter increment with
|
71
|
+
a timestamp of `Time.now`;
|
72
|
+
|
73
|
+
1. If the first argument `is_a? Time`, then store a counter increment with
|
74
|
+
a timestamp of the first argument;
|
75
|
+
|
76
|
+
1. If the first argument has a `#to_str` method, then call that and store
|
77
|
+
the result as a binary (if the `#encoding` method returns `ASCII-8BIT`
|
78
|
+
or the string does not encode cleanly to UTF-8), or as a UTF-8 string
|
79
|
+
otherwise;
|
80
|
+
|
81
|
+
1. If the first argument has a `#integer?` method, then call that, and
|
82
|
+
based on whether the result of that is `true` or `false`, store either
|
83
|
+
an integer or float.
|
84
|
+
|
85
|
+
1. Otherwise, raise `ArgumentError`, as we were unable to determine how to
|
86
|
+
convert the provided argument into something that would be understood.
|
87
|
+
|
88
|
+
|
89
|
+
## Timestamps
|
90
|
+
|
91
|
+
Vaultaire stores all data points with a timestamp. If you want to specify
|
92
|
+
the timestamp that should be associated with your data point, you should
|
93
|
+
pass a `Time` object to `Marquise#send` as the first non-value argument
|
94
|
+
(that is, the first argument if storing a counter increment, or the second
|
95
|
+
argument otherwise). If you do not specify a timestamp, then `Time.now`
|
96
|
+
will be called within the `#send` method, and that time used as the
|
97
|
+
timestamp.
|
98
|
+
|
99
|
+
|
100
|
+
## Tagging data points
|
101
|
+
|
102
|
+
To differentiate data points from different series, Vaultaire allows the
|
103
|
+
specification of arbitrary key/value hashes, which you specify in the usual
|
104
|
+
Rubyesque fashion:
|
105
|
+
|
106
|
+
s.tell(42, :answer => 'ultimate')
|
107
|
+
s.tell(Math::PI, constant: 'yes', closest_to: 3)
|
108
|
+
|
109
|
+
Note that all keys and values will be converted into strings using `#to_s`,
|
110
|
+
because Vaultaire only supports strings in its tag hashes.
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
begin
|
7
|
+
Bundler.setup
|
8
|
+
rescue Bundler::BundlerError => e
|
9
|
+
$stderr.puts e.message
|
10
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
11
|
+
exit e.status_code
|
12
|
+
end
|
13
|
+
|
14
|
+
Bundler::GemHelper.install_tasks
|
15
|
+
|
16
|
+
require 'rdoc/task'
|
17
|
+
|
18
|
+
Rake::RDocTask.new do |rd|
|
19
|
+
rd.main = "README.md"
|
20
|
+
rd.title = 'marquise'
|
21
|
+
rd.rdoc_files.include("README.md", "lib/**/*.rb")
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'git-version-bump/rake-tasks'
|
25
|
+
|
26
|
+
desc "Run guard"
|
27
|
+
task :guard do
|
28
|
+
Bundler.setup(:default, :test)
|
29
|
+
require 'guard'
|
30
|
+
::Guard.start(:clear => true)
|
31
|
+
while ::Guard.running do
|
32
|
+
sleep 0.5
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
require 'rspec/core/rake_task'
|
37
|
+
RSpec::Core::RakeTask.new :test do |t|
|
38
|
+
t.pattern = "spec/**/*_spec.rb"
|
39
|
+
end
|
data/lib/marquise.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'marquise/ffi'
|
2
|
+
require 'ffi/dry/errno'
|
3
|
+
|
4
|
+
# A Vaultaire data point transport
|
5
|
+
#
|
6
|
+
# Instances of `Marquise` send data points that they are told about to a
|
7
|
+
# Vaultaire data store. It has a very simple interface that hides a lot
|
8
|
+
# of complexity.
|
9
|
+
class Marquise
|
10
|
+
include ::FFI::DRY::ErrnoHelper
|
11
|
+
|
12
|
+
# Open a Marquise consumer and (optionally) yield it to a block
|
13
|
+
#
|
14
|
+
# With no associated block, `Marquise.open` is a synonym for
|
15
|
+
# `Marquise.new`. If the optional block is given, a newly created
|
16
|
+
# `Marquise` object will be passed to the block, and will then be
|
17
|
+
# automatically closed when the block terminates, and the value of
|
18
|
+
# the block returned.
|
19
|
+
def self.open(zmq_url, batch_period = 5)
|
20
|
+
m = Marquise.new(zmq_url, batch_period)
|
21
|
+
rv = m
|
22
|
+
|
23
|
+
if block_given?
|
24
|
+
begin
|
25
|
+
rv = yield m
|
26
|
+
ensure
|
27
|
+
m.close
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
rv
|
32
|
+
end
|
33
|
+
|
34
|
+
# Create a new `Marquise` transport object
|
35
|
+
#
|
36
|
+
# `zmq_url` is the URL to your ZeroMQ broker associated with the
|
37
|
+
# Vaultaire system you're dumping data into. `batch_period` is optional
|
38
|
+
# (defaults to `5`) is the number of seconds between "flushes" of data
|
39
|
+
# points to ZeroMQ. It can be a floating-point number if you wish to
|
40
|
+
# have sub-second flushes. Increasing the `batch_period` increases the
|
41
|
+
# possibility of losing data points in the event of a spectacular
|
42
|
+
# failure, but also improves performance.
|
43
|
+
def initialize(zmq_url, batch_period = 5)
|
44
|
+
@consumer = Marquise::FFI.marquise_consumer_new(zmq_url, batch_period)
|
45
|
+
|
46
|
+
if @consumer.nil?
|
47
|
+
raise RuntimeError,
|
48
|
+
"libmarquise failed; check syslog (no, seriously)"
|
49
|
+
end
|
50
|
+
|
51
|
+
@connections = {}
|
52
|
+
|
53
|
+
@janitor = Janitor.new(@consumer, @connections)
|
54
|
+
ObjectSpace.define_finalizer(self, @janitor)
|
55
|
+
end
|
56
|
+
|
57
|
+
def tell(*args)
|
58
|
+
val, ts, opts = parse_tell_opts(args)
|
59
|
+
|
60
|
+
k, v, len = if opts.length == 0
|
61
|
+
[nil, nil, 0]
|
62
|
+
else
|
63
|
+
[
|
64
|
+
Marquise::FFI.pointer_list_from_string_array(opts.keys),
|
65
|
+
Marquise::FFI.pointer_list_from_string_array(opts.values),
|
66
|
+
opts.length
|
67
|
+
]
|
68
|
+
end
|
69
|
+
|
70
|
+
rv = if val.nil?
|
71
|
+
Marquise::FFI.marquise_send_counter(
|
72
|
+
connection,
|
73
|
+
k,
|
74
|
+
v,
|
75
|
+
len,
|
76
|
+
ts.to_f * 1_000_000_000
|
77
|
+
)
|
78
|
+
elsif val.respond_to? :to_str and val.respond_to? :encoding
|
79
|
+
s = val.to_str
|
80
|
+
method = nil
|
81
|
+
|
82
|
+
if s.encoding.to_s == 'ASCII-8BIT' or !s.force_encoding('UTF-8').valid_encoding?
|
83
|
+
method = :marquise_send_binary
|
84
|
+
else
|
85
|
+
method = :marquise_send_text
|
86
|
+
s = s.encode('UTF-8')
|
87
|
+
end
|
88
|
+
|
89
|
+
Marquise::FFI.send(
|
90
|
+
method,
|
91
|
+
connection,
|
92
|
+
k,
|
93
|
+
v,
|
94
|
+
len,
|
95
|
+
s,
|
96
|
+
s.length,
|
97
|
+
ts.to_f * 1_000_000_000
|
98
|
+
)
|
99
|
+
elsif val.respond_to? :integer? and val.integer?
|
100
|
+
if val < -(2**63)+1 or val > (2**63)-1
|
101
|
+
raise ArgumentError,
|
102
|
+
"Integer out of range for Marquise#tell"
|
103
|
+
end
|
104
|
+
|
105
|
+
Marquise::FFI.marquise_send_int(
|
106
|
+
connection,
|
107
|
+
k,
|
108
|
+
v,
|
109
|
+
len,
|
110
|
+
val,
|
111
|
+
ts.to_f * 1_000_000_000
|
112
|
+
)
|
113
|
+
elsif val.respond_to? :integer? and !val.integer?
|
114
|
+
Marquise::FFI.marquise_send_real(
|
115
|
+
connection,
|
116
|
+
k,
|
117
|
+
v,
|
118
|
+
len,
|
119
|
+
val,
|
120
|
+
ts.to_f * 1_000_000_000
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
if rv == -1
|
125
|
+
raise errno_exception
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Close a Marquise instance
|
130
|
+
#
|
131
|
+
# This must be called when you are done with your Marquise instance, to
|
132
|
+
# avoid leaving memory and file descriptors.
|
133
|
+
def close
|
134
|
+
@janitor.call
|
135
|
+
ObjectSpace.undefine_finalizer(self)
|
136
|
+
@connections = {}
|
137
|
+
@consumer = nil
|
138
|
+
end
|
139
|
+
|
140
|
+
# :stopdoc:
|
141
|
+
# Initialize a connection
|
142
|
+
#
|
143
|
+
# You should rarely have to call this method yourself; Marquise will do
|
144
|
+
# it automatically for you when required.
|
145
|
+
def connect
|
146
|
+
th = Thread.current
|
147
|
+
|
148
|
+
return if @connections[th]
|
149
|
+
|
150
|
+
@connections[th] = Marquise::FFI.marquise_connect(@consumer)
|
151
|
+
|
152
|
+
if @connections[th].nil?
|
153
|
+
raise RuntimeError.new("marquise_connect() failed... consult syslog (no, seriously)")
|
154
|
+
end
|
155
|
+
|
156
|
+
nil
|
157
|
+
end
|
158
|
+
|
159
|
+
# Get the connection pointer for the current thread
|
160
|
+
def connection
|
161
|
+
self.connect
|
162
|
+
|
163
|
+
@connections[Thread.current]
|
164
|
+
end
|
165
|
+
|
166
|
+
# A helper class to cleanup Marquise consumers. We can't just do the
|
167
|
+
# obvious, which would be to create a Proc inside `Marquise.initialize`,
|
168
|
+
# because that would leave a reference to the object laying around and
|
169
|
+
# we'd never get GC'd. So we do this instead. I stole this technique
|
170
|
+
# from Tempfile; blame them if this is insane.
|
171
|
+
class Janitor
|
172
|
+
def initialize(ptr, conns)
|
173
|
+
@ptr = ptr
|
174
|
+
@conns = conns
|
175
|
+
end
|
176
|
+
|
177
|
+
def call(*args)
|
178
|
+
@conns.values.each { |c| Marquise::FFI.marquise_close(c) }
|
179
|
+
Marquise::FFI.marquise_consumer_shutdown(@ptr)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
def parse_tell_opts(args)
|
185
|
+
orig_args = args.dup
|
186
|
+
|
187
|
+
val = nil
|
188
|
+
ts = Time.now
|
189
|
+
opts = {}
|
190
|
+
|
191
|
+
if ((args[0].respond_to? :to_str and args[0].respond_to? :encoding) or
|
192
|
+
args[0].respond_to? :integer?)
|
193
|
+
val = args.shift
|
194
|
+
end
|
195
|
+
|
196
|
+
if args[0].is_a? Time
|
197
|
+
ts = args.shift
|
198
|
+
end
|
199
|
+
|
200
|
+
if args[0].is_a? Hash
|
201
|
+
opts = args.shift
|
202
|
+
end
|
203
|
+
|
204
|
+
unless args.empty?
|
205
|
+
raise ArgumentError,
|
206
|
+
"Invalid call to Marquise#tell (you passed '#{orig_args.map(&:inspect).join(', ')}')"
|
207
|
+
end
|
208
|
+
|
209
|
+
[val, ts, opts]
|
210
|
+
end
|
211
|
+
# :startdoc:
|
212
|
+
end
|
data/lib/marquise/ffi.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
|
3
|
+
class Marquise
|
4
|
+
module FFI
|
5
|
+
extend ::FFI::Library
|
6
|
+
ffi_lib 'libmarquise.so.1'
|
7
|
+
|
8
|
+
attach_function :marquise_consumer_new, [ :string, :double ], :pointer
|
9
|
+
attach_function :marquise_consumer_shutdown, [ :pointer ], :void
|
10
|
+
|
11
|
+
attach_function :marquise_connect, [ :pointer ], :pointer
|
12
|
+
attach_function :marquise_close, [ :pointer ], :void
|
13
|
+
|
14
|
+
attach_function :marquise_send_text, [ :pointer,
|
15
|
+
:pointer,
|
16
|
+
:pointer,
|
17
|
+
:size_t,
|
18
|
+
:string,
|
19
|
+
:size_t,
|
20
|
+
:uint64 ],
|
21
|
+
:int
|
22
|
+
attach_function :marquise_send_int, [ :pointer,
|
23
|
+
:pointer,
|
24
|
+
:pointer,
|
25
|
+
:size_t,
|
26
|
+
:int64,
|
27
|
+
:uint64 ],
|
28
|
+
:int
|
29
|
+
attach_function :marquise_send_real, [ :pointer,
|
30
|
+
:pointer,
|
31
|
+
:pointer,
|
32
|
+
:size_t,
|
33
|
+
:double,
|
34
|
+
:uint64 ],
|
35
|
+
:int
|
36
|
+
attach_function :marquise_send_counter, [ :pointer,
|
37
|
+
:pointer,
|
38
|
+
:pointer,
|
39
|
+
:size_t,
|
40
|
+
:uint64 ],
|
41
|
+
:int
|
42
|
+
attach_function :marquise_send_binary, [ :pointer,
|
43
|
+
:pointer,
|
44
|
+
:pointer,
|
45
|
+
:size_t,
|
46
|
+
:pointer,
|
47
|
+
:size_t,
|
48
|
+
:uint64 ],
|
49
|
+
:int
|
50
|
+
|
51
|
+
# Common helper to take an array of strings (or things that we'll turn
|
52
|
+
# into strings) and turn it into an FFI::MemoryPointer containing
|
53
|
+
# FFI::MemoryPointers for each string.
|
54
|
+
def self.pointer_list_from_string_array(ary)
|
55
|
+
ptrs = []
|
56
|
+
ary.each { |e| ptrs << ::FFI::MemoryPointer.from_string(e.to_s) }
|
57
|
+
ptrs << nil
|
58
|
+
|
59
|
+
ptr = ::FFI::MemoryPointer.new(:pointer, ptrs.length)
|
60
|
+
ptrs.each_with_index { |p, i| ptr[i].write_pointer(p) }
|
61
|
+
|
62
|
+
ptr
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|