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