tupelo 0.21 → 0.22
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +171 -45
- data/bin/tup +51 -0
- data/example/counters/merge.rb +23 -3
- data/example/multi-tier/multi-sinatras.rb +5 -0
- data/example/riemann/event-subspace.rb +1 -4
- data/example/riemann/expiration-dbg.rb +2 -0
- data/example/riemann/producer.rb +4 -3
- data/example/riemann/v1/riemann.rb +2 -2
- data/example/riemann/v2/event-template.rb +71 -0
- data/example/riemann/v2/expirer.rb +1 -1
- data/example/riemann/v2/hash-store.rb +1 -0
- data/example/riemann/v2/ordered-event-store.rb +4 -1
- data/example/riemann/v2/riemann.rb +15 -8
- data/example/riemann/v2/sqlite-event-store.rb +117 -72
- data/example/sqlite/poi-store.rb +1 -1
- data/example/sqlite/poi-template.rb +2 -2
- data/example/sqlite/poi-v2.rb +2 -2
- data/example/subspaces/ramp.rb +9 -2
- data/example/tcp.rb +5 -0
- data/example/tiny-tcp-client.rb +15 -0
- data/example/tiny-tcp-service.rb +32 -0
- data/lib/tupelo/app.rb +4 -4
- data/lib/tupelo/app/builder.rb +2 -2
- data/lib/tupelo/app/irb-shell.rb +3 -3
- data/lib/tupelo/archiver.rb +0 -2
- data/lib/tupelo/archiver/tuplestore.rb +1 -1
- data/lib/tupelo/archiver/worker.rb +6 -6
- data/lib/tupelo/client.rb +2 -2
- data/lib/tupelo/client/reader.rb +3 -3
- data/lib/tupelo/client/scheduler.rb +1 -1
- data/lib/tupelo/client/subspace.rb +2 -2
- data/lib/tupelo/client/transaction.rb +28 -28
- data/lib/tupelo/client/tuplestore.rb +2 -2
- data/lib/tupelo/client/worker.rb +11 -10
- data/lib/tupelo/util/bin-circle.rb +8 -8
- data/lib/tupelo/util/boolean.rb +1 -1
- data/lib/tupelo/version.rb +1 -1
- data/test/lib/mock-client.rb +10 -10
- data/test/system/test-archiver.rb +2 -2
- data/test/unit/test-ops.rb +21 -21
- metadata +10 -20
- data/example/bingo/bingo-v2.rb +0 -20
- data/example/broker-queue.rb +0 -35
- data/example/child-of-child.rb +0 -34
- data/example/dataflow.rb +0 -21
- data/example/pregel/dist-opt.rb +0 -15
- data/example/riemann/v2/event-sql.rb +0 -56
- data/example/sqlite/tmp/poi-sqlite.rb +0 -35
- data/example/subspaces/addr-book-v1.rb +0 -104
- data/example/subspaces/addr-book-v2.rb +0 -16
- data/example/subspaces/sorted-set-space-OLD.rb +0 -130
- data/lib/tupelo/tuplets/persistent-archiver.rb +0 -86
- data/lib/tupelo/tuplets/persistent-archiver/tuplespace.rb +0 -91
- data/lib/tupelo/tuplets/persistent-archiver/worker.rb +0 -114
@@ -1,6 +1,6 @@
|
|
1
1
|
# A toy implementation of Riemann (http://riemann.io).
|
2
2
|
#
|
3
|
-
# Version 1 uses the default
|
3
|
+
# Version 1 uses the default tuplestore for all subspaces, which is inefficient
|
4
4
|
# for searching.
|
5
5
|
|
6
6
|
require 'tupelo/app'
|
@@ -28,7 +28,7 @@ Tupelo.application do
|
|
28
28
|
child subscribe: "event", passive: true do
|
29
29
|
log.progname = "consumer #{i}"
|
30
30
|
read subspace("event") do |event|
|
31
|
-
log event
|
31
|
+
log event # add analytics here
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Hard-coded to work with tuples belonging to the "event" subspace
|
2
|
+
# and with the SqliteEventStore table defined. This template is designed
|
3
|
+
# for range queries on service, service and host, or service, host, and time,
|
4
|
+
# using the composite index.
|
5
|
+
class EventTemplate
|
6
|
+
# Template defining the whole event subspace, of which this instance will
|
7
|
+
# match a subset.
|
8
|
+
attr_reader :event_template
|
9
|
+
|
10
|
+
attr_reader :service, :host, :time
|
11
|
+
|
12
|
+
## todo: support queries by tag or custom key
|
13
|
+
|
14
|
+
# Service, host, and time can be intervals or single values or nil to match
|
15
|
+
# any value. The event_template must be a template that matches all tuples
|
16
|
+
# in the event subspace, such as subspace("event"). The EventTemplate will
|
17
|
+
# match a subset of this subspace.
|
18
|
+
def initialize event_template, service: nil, host: nil, time: nil
|
19
|
+
@event_template = event_template
|
20
|
+
@service = service
|
21
|
+
@host = host
|
22
|
+
@time = time
|
23
|
+
end
|
24
|
+
|
25
|
+
# We only need to define this method if we plan to wait for event tuples
|
26
|
+
# locally using this template, i.e. read(template) or take(template).
|
27
|
+
# Non-waiting queries (such as read_all) just use #find_in or #find_all_in.
|
28
|
+
def === tuple
|
29
|
+
@event_template === tuple and
|
30
|
+
!@service || @service === tuple[:service] and
|
31
|
+
!@host || @host === tuple[:host] and
|
32
|
+
!@time || @time === tuple[:time]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a dataset corresponding to this particular template.
|
36
|
+
# Dataset has all columns, including id, because we need id to
|
37
|
+
# populate tags and custom key-value data, if any.
|
38
|
+
def dataset store
|
39
|
+
where_terms = {}
|
40
|
+
where_terms[:service] = @service if @service
|
41
|
+
where_terms[:host] = @host if @host
|
42
|
+
where_terms[:time] = @time if @time
|
43
|
+
store.events.where(where_terms)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Optimized search function to find a template match that exists already in
|
47
|
+
# the table. For operations that wait for a match, #=== is used instead.
|
48
|
+
def find_in store, distinct_from: []
|
49
|
+
matches = dataset(store).limit(distinct_from.size + 1).all
|
50
|
+
|
51
|
+
distinct_from.each do |tuple|
|
52
|
+
if i=matches.index(tuple)
|
53
|
+
matches.delete_at i
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
store.repopulate(matches.first)
|
58
|
+
end
|
59
|
+
|
60
|
+
def find_all_in store
|
61
|
+
if block_given?
|
62
|
+
dataset(store).each do |tuple|
|
63
|
+
yield store.repopulate(tuple)
|
64
|
+
end
|
65
|
+
else
|
66
|
+
dataset(store).map do |tuple|
|
67
|
+
store.repopulate(tuple)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -16,7 +16,7 @@ class Tupelo::Client
|
|
16
16
|
loop do
|
17
17
|
event = read subspace("event") # event with lowest time+ttl
|
18
18
|
# This just happens to be true, but alternately we could define
|
19
|
-
# OrderedEventTemplate.
|
19
|
+
# OrderedEventTemplate.first() to make a template that explicitly
|
20
20
|
# finds the lowest key.
|
21
21
|
dt_next_expiration = event["time"] + event["ttl"] - Time.now.to_f
|
22
22
|
begin
|
@@ -4,6 +4,7 @@ require 'tupelo/archiver/tuplestore'
|
|
4
4
|
# rather than matching with templates or other queries.
|
5
5
|
# See also example/multitier/kvstore.rb.
|
6
6
|
class HashStore < Tupelo::Archiver::TupleStore
|
7
|
+
# Same as parent, but default the zero_tolerance to 1000.
|
7
8
|
def initialize zero_tolerance: 1000
|
8
9
|
super
|
9
10
|
end
|
@@ -2,6 +2,9 @@ require 'rbtree'
|
|
2
2
|
|
3
3
|
# Hard-coded to work with tuples belonging to the "event" subspace defined in
|
4
4
|
# event-subspace.rb and with the OrderedEventStore data structure defined below.
|
5
|
+
# This class only exposes the "find one item before" functionality of rbtree,
|
6
|
+
# and not range searches etc., because this is all that is needed for the
|
7
|
+
# expirer.
|
5
8
|
class OrderedEventTemplate
|
6
9
|
attr_reader :expiration
|
7
10
|
|
@@ -46,7 +49,7 @@ class OrderedEventStore
|
|
46
49
|
|
47
50
|
attr_reader :tree, :metas
|
48
51
|
|
49
|
-
def initialize
|
52
|
+
def initialize client: nil
|
50
53
|
clear
|
51
54
|
end
|
52
55
|
|
@@ -3,12 +3,17 @@
|
|
3
3
|
# Version 2 stores the event subspace using different data
|
4
4
|
# structures in different clients, depending on needs:
|
5
5
|
#
|
6
|
-
# * generic consumers need to index by host and
|
6
|
+
# * generic consumers need to index by service, host, and time,
|
7
|
+
# by tags, and by custom keys, so they use in-memory sqlite with
|
8
|
+
# normalized tables.
|
7
9
|
#
|
8
|
-
# * the expiration manager
|
10
|
+
# * the expiration manager needs to sort by expiration time, so
|
11
|
+
# it uses an rbtree (an in-memory Red-Black binary tree),
|
12
|
+
# for a good balance of insert and lookup performance.
|
9
13
|
#
|
10
14
|
# * the critical event alerter doesn't need to sort or search at all,
|
11
|
-
# it just needs efficient insert and delete
|
15
|
+
# it just needs efficient insert and delete, so it uses a store
|
16
|
+
# based on a simple hash table.
|
12
17
|
#
|
13
18
|
# Run with --http to expose a web API and run a test web client.
|
14
19
|
#
|
@@ -31,12 +36,12 @@ require_relative 'hash-store'
|
|
31
36
|
require_relative 'sqlite-event-store'
|
32
37
|
|
33
38
|
N_PRODUCERS = 3
|
34
|
-
N_CONSUMERS =
|
39
|
+
N_CONSUMERS = 1
|
35
40
|
|
36
41
|
Tupelo.application do
|
37
42
|
local do
|
38
43
|
define_event_subspace
|
39
|
-
EVENT_SPACE =
|
44
|
+
EVENT_SPACE = subspace("event")
|
40
45
|
end
|
41
46
|
|
42
47
|
if USE_HTTP
|
@@ -56,11 +61,13 @@ Tupelo.application do
|
|
56
61
|
|
57
62
|
N_CONSUMERS.times do |i|
|
58
63
|
# stores events indexed by host, service
|
59
|
-
child tuplestore: [SqliteEventStore, EVENT_SPACE],
|
60
|
-
subscribe: "event",
|
64
|
+
child tuplestore: [SqliteEventStore, EVENT_SPACE.spec],
|
65
|
+
subscribe: "event",
|
66
|
+
symbolize_keys: true, # for ease of use with sequel DB interface
|
67
|
+
passive: true do
|
61
68
|
log.progname = "consumer #{i}"
|
62
69
|
read subspace("event") do |event|
|
63
|
-
log.info event
|
70
|
+
log.info event # add analytics here
|
64
71
|
end
|
65
72
|
end
|
66
73
|
end
|
@@ -1,49 +1,5 @@
|
|
1
1
|
require 'sequel'
|
2
|
-
|
3
|
-
# Hard-coded to work with tuples belonging to the "event" subspace
|
4
|
-
# and with the SqliteEventStore table defined below. This template is designed
|
5
|
-
# for range queries on host, or on host and service, using the composite index.
|
6
|
-
class EventTemplate
|
7
|
-
attr_reader :host, :service, :space
|
8
|
-
|
9
|
-
# host and service can be intervals or single values or nil to match any value
|
10
|
-
def initialize host: nil, service: nil, space: nil
|
11
|
-
@host = host
|
12
|
-
@service = service
|
13
|
-
@space = space
|
14
|
-
end
|
15
|
-
|
16
|
-
# we only need to define this method if we plan to wait for event tuples
|
17
|
-
# locally using this template, i.e. read(template) or take(template).
|
18
|
-
# Non-waiting queries (such as read_all) just use #find_in.
|
19
|
-
def === tuple
|
20
|
-
@space === tuple and
|
21
|
-
!@host || @host === tuple["host"] and
|
22
|
-
!@service || @service === tuple["service"]
|
23
|
-
end
|
24
|
-
|
25
|
-
# Optimized search function to find a template match that exists already in
|
26
|
-
# the table. For operations that wait for a match, #=== is used instead.
|
27
|
-
def find_in events, distinct_from: []
|
28
|
-
where_terms = {}
|
29
|
-
where_terms[:host] = @host if @host
|
30
|
-
where_terms[:service] = @service if @service
|
31
|
-
|
32
|
-
matches = events.
|
33
|
-
### select all but id
|
34
|
-
where(where_terms).
|
35
|
-
limit(distinct_from.size + 1).all
|
36
|
-
|
37
|
-
distinct_from.each do |tuple|
|
38
|
-
if i=matches.index(tuple)
|
39
|
-
matches.delete_at i
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
matches.first ## get the tags and customs
|
44
|
-
### convert sym to string?
|
45
|
-
end
|
46
|
-
end
|
2
|
+
require_relative 'event-template'
|
47
3
|
|
48
4
|
# A tuple store that is optimized for event data. These tuples are stored in an
|
49
5
|
# in-memory sqlite database table. Tuples that do not fit this pattern (such
|
@@ -51,20 +7,33 @@ end
|
|
51
7
|
class SqliteEventStore
|
52
8
|
include Enumerable
|
53
9
|
|
54
|
-
attr_reader :events, :
|
10
|
+
attr_reader :events, :tags, :customs, :alt_customs, :metas
|
11
|
+
|
12
|
+
# Template for matching all event tuples.
|
13
|
+
attr_reader :event_template
|
14
|
+
|
15
|
+
# Object with #dump and #load methods used to put the custom hash into
|
16
|
+
# a sqlite string column.
|
17
|
+
attr_reader :blobber
|
18
|
+
|
19
|
+
def initialize spec, client: nil
|
20
|
+
@event_template = client.worker.pot_for(spec)
|
21
|
+
@blobber = client.blobber
|
22
|
+
# calling #pot_for in client means that resulting template
|
23
|
+
# will have keys converted as needed (in the case of this client,
|
24
|
+
# to symbols).
|
55
25
|
|
56
|
-
# space should be client.subspace("event"), but really we only need
|
57
|
-
# `space.pot` the portable object template for deciding membership.
|
58
|
-
def initialize space
|
59
|
-
@space = space
|
60
26
|
clear
|
27
|
+
|
28
|
+
# To be more general, we could inspect the spec to determine which keys to
|
29
|
+
# use when creating and querying the table
|
61
30
|
end
|
62
31
|
|
63
32
|
def clear
|
64
33
|
@db = Sequel.sqlite
|
65
34
|
@db.create_table :events do
|
66
35
|
primary_key :id # id is not significant to our app
|
67
|
-
text :host, null: false
|
36
|
+
text :host, null: false
|
68
37
|
text :service, null: false
|
69
38
|
text :state
|
70
39
|
number :time
|
@@ -72,7 +41,7 @@ class SqliteEventStore
|
|
72
41
|
number :metric
|
73
42
|
number :ttl
|
74
43
|
|
75
|
-
index [:host, :
|
44
|
+
index [:service, :host, :time]
|
76
45
|
end
|
77
46
|
|
78
47
|
@db.create_table :tags do
|
@@ -85,31 +54,86 @@ class SqliteEventStore
|
|
85
54
|
@db.create_table :customs do
|
86
55
|
foreign_key :event_id, :events
|
87
56
|
text :key
|
88
|
-
text :
|
57
|
+
text :value_blob
|
89
58
|
index :key
|
90
59
|
primary_key [:event_id, :key]
|
91
60
|
end
|
92
61
|
|
93
|
-
@events
|
62
|
+
@events = @db[:events]
|
63
|
+
@tags = @db[:tags]
|
64
|
+
@customs = @db[:customs]
|
65
|
+
|
66
|
+
@alt_customs = Hash.new {|h,k| h[k] = {}}
|
67
|
+
# Alternate store for any custom that has a non-string key. To be
|
68
|
+
# completely correct, we have to support this because the event space
|
69
|
+
# requires it.
|
70
|
+
#
|
71
|
+
# Contains
|
72
|
+
# {event_id => {key => value}}
|
73
|
+
# where key (and value, of course) are arbitrary tuple data.
|
74
|
+
# We don't have the key index like in the customs table, but that's
|
75
|
+
# ok since the key isn't a string.
|
76
|
+
|
94
77
|
@metas = []
|
95
78
|
end
|
96
79
|
|
97
|
-
def
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
80
|
+
def collect_tags event_id
|
81
|
+
tags.
|
82
|
+
select(:tag).
|
83
|
+
where(event_id: event_id).
|
84
|
+
map {|row| row[:tag]}
|
85
|
+
end
|
86
|
+
|
87
|
+
def collect_custom event_id
|
88
|
+
custom = customs.
|
89
|
+
select(:key, :value_blob).
|
90
|
+
where(event_id: event_id).
|
91
|
+
inject({}) {|h, row|
|
92
|
+
h[row[:key].to_sym] = blobber.load(row[:value_blob])
|
93
|
+
h
|
94
|
+
}
|
95
|
+
|
96
|
+
if alt_customs.key?(event_id)
|
97
|
+
custom.merge! alt_customs[event_id]
|
106
98
|
end
|
99
|
+
|
100
|
+
custom
|
101
|
+
end
|
102
|
+
|
103
|
+
def repopulate tuple
|
104
|
+
event_id = tuple.delete :id
|
105
|
+
tuple[:tags] = collect_tags(event_id)
|
106
|
+
tuple[:custom] = collect_custom(event_id)
|
107
|
+
tuple
|
108
|
+
end
|
109
|
+
|
110
|
+
def each
|
111
|
+
events.each {|tuple| yield repopulate(tuple)}
|
112
|
+
metas.each {|tuple| yield tuple}
|
107
113
|
end
|
108
114
|
|
109
115
|
def insert tuple
|
110
116
|
case tuple
|
111
|
-
when
|
112
|
-
|
117
|
+
when event_template
|
118
|
+
tuple = tuple.dup
|
119
|
+
tuple_tags = tuple.delete :tags
|
120
|
+
tuple_custom = tuple.delete :custom
|
121
|
+
|
122
|
+
event_id = events.insert(tuple)
|
123
|
+
|
124
|
+
tuple_tags.each do |tag|
|
125
|
+
tags << {event_id: event_id, tag: tag}
|
126
|
+
end
|
127
|
+
|
128
|
+
tuple_custom.each do |key, value|
|
129
|
+
if key.kind_of? Symbol
|
130
|
+
blob = Sequel.blob(blobber.dump(value))
|
131
|
+
customs << {event_id: event_id, key: key.to_s, value_blob: blob}
|
132
|
+
else
|
133
|
+
alt_customs[event_id][key] = value
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
113
137
|
else
|
114
138
|
metas << tuple
|
115
139
|
end
|
@@ -117,16 +141,27 @@ class SqliteEventStore
|
|
117
141
|
|
118
142
|
def delete_once tuple
|
119
143
|
case tuple
|
120
|
-
when
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
144
|
+
when event_template
|
145
|
+
tuple = tuple.dup
|
146
|
+
tuple_tags = tuple.delete :tags
|
147
|
+
tuple_custom = tuple.delete :custom
|
148
|
+
|
149
|
+
event_id = events.select(:id).where(tuple).limit(1)
|
150
|
+
count = events.where(id: event_id).count
|
125
151
|
|
126
152
|
if count == 0
|
127
153
|
false
|
128
154
|
elsif count == 1
|
129
|
-
|
155
|
+
if tuple_tags.sort == collect_tags(event_id).sort && ## avoid sort?
|
156
|
+
tuple_custom == collect_custom(event_id)
|
157
|
+
tags.where(event_id: event_id).delete
|
158
|
+
customs.where(event_id: event_id).delete
|
159
|
+
alt_customs.delete(event_id)
|
160
|
+
events.where(id: event_id).delete
|
161
|
+
true
|
162
|
+
else
|
163
|
+
false
|
164
|
+
end
|
130
165
|
else
|
131
166
|
raise "internal error: primary key, id, was not unique"
|
132
167
|
end
|
@@ -157,7 +192,7 @@ class SqliteEventStore
|
|
157
192
|
def find_match_for template, distinct_from: []
|
158
193
|
case template
|
159
194
|
when EventTemplate
|
160
|
-
template.find_in
|
195
|
+
template.find_in self, distinct_from: distinct_from
|
161
196
|
else
|
162
197
|
## if template can match subspace
|
163
198
|
# Fall back to linear search, same as default tuplestore.
|
@@ -171,4 +206,14 @@ class SqliteEventStore
|
|
171
206
|
## end
|
172
207
|
end
|
173
208
|
end
|
209
|
+
|
210
|
+
def find_all_matches_for template, &bl
|
211
|
+
case template
|
212
|
+
when EventTemplate
|
213
|
+
template.find_all_in self, &bl
|
214
|
+
else
|
215
|
+
# Fall back to linear search using #each and #===.
|
216
|
+
grep template, &bl
|
217
|
+
end
|
218
|
+
end
|
174
219
|
end
|
data/example/sqlite/poi-store.rb
CHANGED
@@ -7,10 +7,10 @@ class PoiTemplate
|
|
7
7
|
attr_reader :poi_template
|
8
8
|
|
9
9
|
# lat and lng can be intervals or single values or nil to match any value
|
10
|
-
def initialize lat: nil, lng: nil
|
10
|
+
def initialize poi_template, lat: nil, lng: nil
|
11
|
+
@poi_template = poi_template
|
11
12
|
@lat = lat
|
12
13
|
@lng = lng
|
13
|
-
@poi_template = poi_template
|
14
14
|
end
|
15
15
|
|
16
16
|
# we only need to define this method if we plan to wait for poi tuples
|