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 ADDED
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec
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
@@ -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