tupelo 0.21 → 0.22
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.
- 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
|