tupelo 0.21 → 0.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +171 -45
  3. data/bin/tup +51 -0
  4. data/example/counters/merge.rb +23 -3
  5. data/example/multi-tier/multi-sinatras.rb +5 -0
  6. data/example/riemann/event-subspace.rb +1 -4
  7. data/example/riemann/expiration-dbg.rb +2 -0
  8. data/example/riemann/producer.rb +4 -3
  9. data/example/riemann/v1/riemann.rb +2 -2
  10. data/example/riemann/v2/event-template.rb +71 -0
  11. data/example/riemann/v2/expirer.rb +1 -1
  12. data/example/riemann/v2/hash-store.rb +1 -0
  13. data/example/riemann/v2/ordered-event-store.rb +4 -1
  14. data/example/riemann/v2/riemann.rb +15 -8
  15. data/example/riemann/v2/sqlite-event-store.rb +117 -72
  16. data/example/sqlite/poi-store.rb +1 -1
  17. data/example/sqlite/poi-template.rb +2 -2
  18. data/example/sqlite/poi-v2.rb +2 -2
  19. data/example/subspaces/ramp.rb +9 -2
  20. data/example/tcp.rb +5 -0
  21. data/example/tiny-tcp-client.rb +15 -0
  22. data/example/tiny-tcp-service.rb +32 -0
  23. data/lib/tupelo/app.rb +4 -4
  24. data/lib/tupelo/app/builder.rb +2 -2
  25. data/lib/tupelo/app/irb-shell.rb +3 -3
  26. data/lib/tupelo/archiver.rb +0 -2
  27. data/lib/tupelo/archiver/tuplestore.rb +1 -1
  28. data/lib/tupelo/archiver/worker.rb +6 -6
  29. data/lib/tupelo/client.rb +2 -2
  30. data/lib/tupelo/client/reader.rb +3 -3
  31. data/lib/tupelo/client/scheduler.rb +1 -1
  32. data/lib/tupelo/client/subspace.rb +2 -2
  33. data/lib/tupelo/client/transaction.rb +28 -28
  34. data/lib/tupelo/client/tuplestore.rb +2 -2
  35. data/lib/tupelo/client/worker.rb +11 -10
  36. data/lib/tupelo/util/bin-circle.rb +8 -8
  37. data/lib/tupelo/util/boolean.rb +1 -1
  38. data/lib/tupelo/version.rb +1 -1
  39. data/test/lib/mock-client.rb +10 -10
  40. data/test/system/test-archiver.rb +2 -2
  41. data/test/unit/test-ops.rb +21 -21
  42. metadata +10 -20
  43. data/example/bingo/bingo-v2.rb +0 -20
  44. data/example/broker-queue.rb +0 -35
  45. data/example/child-of-child.rb +0 -34
  46. data/example/dataflow.rb +0 -21
  47. data/example/pregel/dist-opt.rb +0 -15
  48. data/example/riemann/v2/event-sql.rb +0 -56
  49. data/example/sqlite/tmp/poi-sqlite.rb +0 -35
  50. data/example/subspaces/addr-book-v1.rb +0 -104
  51. data/example/subspaces/addr-book-v2.rb +0 -16
  52. data/example/subspaces/sorted-set-space-OLD.rb +0 -130
  53. data/lib/tupelo/tuplets/persistent-archiver.rb +0 -86
  54. data/lib/tupelo/tuplets/persistent-archiver/tuplespace.rb +0 -91
  55. 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 tuplespace for all subspaces, which is inefficient
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 ### need filtering, actions, etc.
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.frist() to pass in a template that explicitly
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 service
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 need to sort by expiration time
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 = 2
39
+ N_CONSUMERS = 1
35
40
 
36
41
  Tupelo.application do
37
42
  local do
38
43
  define_event_subspace
39
- EVENT_SPACE = client.subspace("event")
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", passive: true do
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 ### need filtering, actions, etc.
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, :metas, :space
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 ## need this ?
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, :service]
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 :value
57
+ text :value_blob
89
58
  index :key
90
59
  primary_key [:event_id, :key]
91
60
  end
92
61
 
93
- @events = @db[: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 each
98
- events.each do |row|
99
- ## extra queries for tags and custom
100
- # yuck, gotta convert symbol keys to string keys:
101
- tuple = row.inject({}) {|h, (k,v)| h[k.to_s] = v; h}
102
- yield tuple
103
- end
104
- metas.each do |tuple|
105
- yield tuple
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 space
112
- events << tuple ## minus tags and customs
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 space
121
- # yuck, gotta convert string keys to symbol keys:
122
- row = tuple.inject({}) {|h, (k,v)| h[k.to_sym] = v; h}
123
- id = events.select(:id).where(row).limit(1)
124
- count = events.where(id: id).delete
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
- true
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 events, distinct_from: distinct_from
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
@@ -40,7 +40,7 @@ class PoiStore
40
40
 
41
41
  def clear
42
42
  @db = Sequel.sqlite
43
- @db.create_table "poi" do
43
+ @db.create_table :poi do
44
44
  primary_key :id # id is not significant to our app
45
45
  float :lat, null: false
46
46
  float :lng, null: false
@@ -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, poi_template: 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