marquise 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.
- 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
|