tupelo 0.19 → 0.20

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +98 -36
  3. data/bin/tup +1 -7
  4. data/bugs/take-write.rb +8 -0
  5. data/example/bingo/bingo-v2.rb +20 -0
  6. data/example/broker-queue.rb +35 -0
  7. data/example/child-of-child.rb +34 -0
  8. data/example/consistent-hash.rb +0 -2
  9. data/example/counters/lock.rb +24 -0
  10. data/example/counters/merge.rb +35 -0
  11. data/example/counters/optimistic.rb +29 -0
  12. data/example/dataflow.rb +21 -0
  13. data/example/dedup.rb +45 -0
  14. data/example/map-reduce/ex.rb +32 -0
  15. data/example/multi-tier/memo2.rb +0 -2
  16. data/example/pregel/dist-opt.rb +15 -0
  17. data/example/riemann/event-subspace.rb +2 -0
  18. data/example/riemann/expiration-dbg.rb +15 -0
  19. data/example/riemann/producer.rb +34 -13
  20. data/example/riemann/v1/expirer.rb +28 -0
  21. data/example/riemann/{riemann-v1.rb → v1/riemann.rb} +5 -8
  22. data/example/riemann/v2/expirer.rb +31 -0
  23. data/example/riemann/v2/hash-store.rb +33 -0
  24. data/example/riemann/v2/http-mode.rb +53 -0
  25. data/example/riemann/v2/ordered-event-store.rb +128 -0
  26. data/example/riemann/{riemann-v2.rb → v2/riemann.rb} +32 -17
  27. data/example/sqlite/poi-store.rb +160 -0
  28. data/example/sqlite/poi-v2.rb +58 -0
  29. data/example/sqlite/poi.rb +40 -0
  30. data/example/sqlite/tmp/poi-sqlite.rb +33 -0
  31. data/example/subspaces/addr-book-v1.rb +0 -2
  32. data/example/subspaces/addr-book-v2.rb +0 -2
  33. data/example/subspaces/addr-book.rb +0 -2
  34. data/example/subspaces/pubsub.rb +0 -2
  35. data/example/subspaces/ramp.rb +0 -2
  36. data/example/subspaces/shop/shop-v2.rb +0 -2
  37. data/example/subspaces/simple.rb +0 -1
  38. data/example/subspaces/sorted-set-space.rb +5 -0
  39. data/lib/tupelo/app.rb +8 -0
  40. data/lib/tupelo/archiver/persistent-tuplespace.rb +2 -2
  41. data/lib/tupelo/archiver/tuplespace.rb +2 -2
  42. data/lib/tupelo/client/reader.rb +18 -8
  43. data/lib/tupelo/client/subspace.rb +12 -4
  44. data/lib/tupelo/client/transaction.rb +13 -1
  45. data/lib/tupelo/client/worker.rb +27 -4
  46. data/lib/tupelo/client.rb +3 -5
  47. data/lib/tupelo/tuplets/persistent-archiver/tuplespace.rb +5 -0
  48. data/lib/tupelo/version.rb +1 -1
  49. data/test/lib/mock-client.rb +1 -0
  50. metadata +26 -7
  51. data/example/riemann/expirer-v1.rb +0 -25
@@ -1,9 +1,8 @@
1
1
  class Tupelo::Client
2
- # Generate some events.
3
- def run_producer i
4
- event = {
2
+ def base_event
3
+ @base_event ||= {
5
4
  host: `hostname`.chomp,
6
- service: "service #{i}",
5
+ service: "process #$$",
7
6
  state: "",
8
7
  time: 0,
9
8
  description: "",
@@ -12,21 +11,43 @@ class Tupelo::Client
12
11
  ttl: 0,
13
12
  custom: nil
14
13
  }.freeze
14
+ end
15
+
16
+ def write_event event
17
+ if event[:ttl] == 0.0
18
+ pulse event # no need to bother with expiration
19
+ else
20
+ write event
21
+ end
15
22
 
16
- e_ok = event.merge(
23
+ log.info "created event #{event}"
24
+ end
25
+
26
+ # Generate some events.
27
+ def run_producer i
28
+ e_ok = base_event.merge(
29
+ service: "service #{i}",
17
30
  state: "ok",
18
31
  time: Time.now.to_f,
19
32
  ttl: 0.2
20
33
  )
34
+ write_event e_ok
21
35
 
22
- if e_ok[:ttl] == 0.0
23
- pulse e_ok # no need to bother with expiration
24
- else
25
- write e_ok
36
+ 30.times do |ei|
37
+ e_cpu = base_event.merge(
38
+ service: "service #{i}",
39
+ state: ei==10 ? "critical" : "ok",
40
+ time: Time.now.to_f,
41
+ ttl: 0.2,
42
+ tags: ["cpu", "cumulative"],
43
+ metric: Process.times.utime + Process.times.stime
44
+ )
45
+ write_event e_cpu
46
+ sleep 0.2
26
47
  end
27
-
28
- log "created event #{e_ok}"
29
-
30
- sleep 0.5 # Let it expire
48
+
49
+ sleep 0.5
50
+ # Make sure whole set of processes stay alive long enough to see
51
+ # expiration happen.
31
52
  end
32
53
  end
@@ -0,0 +1,28 @@
1
+ class Tupelo::Client
2
+ # Expire old events. Uses a scheduler (which is a thread plus an rbtree)
3
+ # to keep track of the expiration times and events.
4
+ def run_expirer_v1
5
+ scheduler = make_scheduler
6
+
7
+ read subspace("event") do |event|
8
+ next if event["state"] == "expired"
9
+
10
+ event_exp = event["time"] + event["ttl"]
11
+ scheduler.at event_exp do
12
+ transaction do
13
+ take_nowait event or break
14
+ # Be cautious, in case of other expirer. If you can rule out
15
+ # this possibility, then `take event` is fine.
16
+ pulse event.merge("state" => "expired")
17
+ # Not sure if this is riemann semantics. Using #pulse rather
18
+ # than #write means that the expired event exists in the
19
+ # space only while the transaction is executing, but that
20
+ # momentary existence is enough to trigger any client that
21
+ # is waiting on a template that matches the event. Use the
22
+ # --debug-expiration switch to see this happening (and use
23
+ # -v to make log messages verbose, showing timestamps).
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -4,17 +4,15 @@
4
4
  # for searching.
5
5
 
6
6
  require 'tupelo/app'
7
- require_relative 'event-subspace'
8
- require_relative 'producer'
9
- require_relative 'expirer-v1'
7
+ require_relative '../event-subspace'
8
+ require_relative '../producer'
9
+ require_relative 'expirer'
10
10
 
11
11
  N_PRODUCERS = 3
12
12
  N_CONSUMERS = 2
13
13
 
14
14
  Tupelo.application do
15
-
16
15
  local do
17
- use_subspaces!
18
16
  define_event_subspace
19
17
  end
20
18
 
@@ -45,11 +43,10 @@ Tupelo.application do
45
43
 
46
44
  if argv.include?("--debug-expiration")
47
45
  # expired event debugger
46
+ require_relative '../expiration-dbg'
48
47
  child subscribe: "event", passive: true do
49
48
  log.progname = "expiration debugger"
50
- read Tupelo::Client::EXPIRED_EVENT do |event|
51
- log event
52
- end
49
+ run_expiration_debugger
53
50
  end
54
51
  end
55
52
 
@@ -0,0 +1,31 @@
1
+ require_relative 'ordered-event-store'
2
+
3
+ class Tupelo::Client
4
+ # Expire old events.
5
+ #
6
+ # A little more complex than v1, but more efficient.
7
+ #
8
+ # This version uses the rbtree to manage the space itself (in the worker
9
+ # thread), instead of using an rbtree to manage the scheduler (in the client
10
+ # thread). It also uses a custom template class to perform range-based queries
11
+ # of the rbtree key, which is not explicitly stored in the tuples. The rbree
12
+ # key is, however, the sum of two values (time + ttl) in the tuples, so the
13
+ # template is still selecting tuples based on their contents.
14
+ #
15
+ def run_expirer_v2
16
+ loop do
17
+ event = read subspace("event") # event with lowest time+ttl
18
+ dt_next_expiration = event["time"] + event["ttl"] - Time.now.to_f
19
+ begin
20
+ transaction timeout: dt_next_expiration do
21
+ read OrderedEventTemplate.before(event)
22
+ end
23
+ rescue TimeoutError
24
+ transaction do
25
+ take_nowait event or break # see note in v1/expirer.rb
26
+ pulse event.merge("state" => "expired")
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ require 'tupelo/archiver/tuplespace'
2
+
3
+ # Store for any kind of tuple, and faster for lookups of literal tuples,
4
+ # rather than matching with templates or other queries.
5
+ # See also example/multitier/kvspace.rb.
6
+ class HashStore < Tupelo::Archiver::Tuplespace
7
+ def initialize zero_tolerance: 1000
8
+ super
9
+ end
10
+
11
+ def find_match_for template, distinct_from: []
12
+ case template
13
+ when Array, Hash # just a tuple
14
+ super
15
+
16
+ else
17
+ # We added this case to Archiver::Tuplespace just so the read(..)
18
+ # will work correctly on tuples that are already in the space
19
+ # when this process starts up. After that point, incoming tuples
20
+ # are matched directly against the CRITICAL_EVENT template without
21
+ # searching the space.
22
+
23
+ # We're not going to use Client#take in this client, so there's no need
24
+ # to handle the distinct_from keyword argument.
25
+ raise "internal error" unless distinct_from.empty?
26
+
27
+ find do |tuple|
28
+ template === tuple
29
+ end
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,53 @@
1
+ # Run a web client -- depends only on http, not tupelo.
2
+ def start_web_client
3
+ fork do
4
+ require 'http'
5
+
6
+ url = 'http://localhost:4567'
7
+
8
+ print "trying server at #{url}"
9
+ begin
10
+ print "."
11
+ HTTP.get url
12
+ rescue Errno::ECONNREFUSED
13
+ sleep 0.2
14
+ retry
15
+ end
16
+
17
+ puts
18
+ puts HTTP.get "#{url}/read"
19
+ HTTP.get "#{url}/exit"
20
+ end
21
+ end
22
+
23
+ # Run a little API server using +client+ to access tupelo.
24
+ def run_web_server(client)
25
+ require 'sinatra/base'
26
+ require 'json'
27
+
28
+ Class.new(Sinatra::Base).class_eval do
29
+ get '/read' do
30
+ host = params["host"] # nil is ok -- matches all hosts
31
+ resp = client.read(
32
+ host: host,
33
+ service: nil,
34
+ state: nil,
35
+ time: nil,
36
+ description: nil,
37
+ tags: nil,
38
+ metric: nil,
39
+ ttl: nil,
40
+ custom: nil
41
+ )
42
+ resp.to_json + "\n"
43
+ end
44
+ ## need way to query by existence of tag
45
+
46
+ get '/exit' do
47
+ Thread.new {sleep 1; exit}
48
+ "bye\n"
49
+ end
50
+
51
+ run!
52
+ end
53
+ end
@@ -0,0 +1,128 @@
1
+ require 'rbtree'
2
+
3
+ # Hard-coded to work with tuples belonging to the "event" subspace defined in
4
+ # event-subspace.rb and with the OrderedEventStore data structure defined below.
5
+ class OrderedEventTemplate
6
+ attr_reader :expiration
7
+
8
+ def self.before event
9
+ new(event: event)
10
+ end
11
+
12
+ def initialize event: event
13
+ time = event["time"] || event[:time]
14
+ ttl = event["ttl"] || event[:ttl]
15
+ # We check symbol keys in case of manually specified event, rather than
16
+ # tuples that passed thru tupelo.
17
+ @expiration = time + ttl
18
+ end
19
+
20
+ def === other
21
+ begin
22
+ other["time"] + other["ttl"] < @expiration
23
+ rescue
24
+ false
25
+ end
26
+ # Should check that subspace("event") === other, but in this example
27
+ # we don't need to, we just make sure it's got time and ttl keys.
28
+ # Anyway, the #=== method is not used, since in this case
29
+ # we just lookup using RBTree#first. See the special case for
30
+ # OrderedEventTemplate in OrderedEventStore#find_match_for.
31
+ # This implementation is just for completeness.
32
+ end
33
+ end
34
+
35
+ # A tuple store (in-memory) that is optimized for events and for searching them
36
+ # in expiration order. This is very much a special case and not reusable for
37
+ # other spaces/stores without modification.
38
+ #
39
+ # This store also manages meta tuples, which it keeps in an array,
40
+ # just like the default Tuplespace class does. Actually, any tuple for which
41
+ # `tuple["time"] + tuple["ttl"]` raises an exception will go in the metas,
42
+ # but in this example the process only subscribes to events and metas.
43
+ #
44
+ class OrderedEventStore
45
+ include Enumerable
46
+
47
+ attr_reader :tree, :metas
48
+
49
+ def initialize
50
+ clear
51
+ end
52
+
53
+ def clear
54
+ @tree = RBTree.new{|t,k| t[k] = []}
55
+ @metas = []
56
+ # We are automatically subscribed to tupelo metadata (subspace defs), so
57
+ # we need to keep them somewhere.
58
+ end
59
+
60
+ def each
61
+ tree.each do |k, events|
62
+ events.each do |event|
63
+ yield event
64
+ end
65
+ end
66
+ metas.each do |tuple|
67
+ yield tuple
68
+ end
69
+ end
70
+
71
+ def insert tuple
72
+ k = tuple["time"] + tuple["ttl"]
73
+ rescue
74
+ metas << tuple
75
+ else
76
+ tree[k] << tuple
77
+ end
78
+
79
+ def delete_once tuple
80
+ k = tuple["time"] + tuple["ttl"]
81
+ rescue
82
+ if i=metas.index(tuple)
83
+ metas.delete_at i
84
+ end
85
+ else
86
+ if tree.key?(k) and tree[k].include? tuple
87
+ tree[k].delete tuple
88
+ tree.delete k if tree[k].empty?
89
+ true
90
+ else
91
+ false
92
+ end
93
+ end
94
+
95
+ def transaction inserts: [], deletes: [], tick: nil
96
+ deletes.each do |tuple|
97
+ delete_once tuple or raise "bug"
98
+ end
99
+
100
+ inserts.each do |tuple|
101
+ insert tuple.freeze ## should be deep_freeze
102
+ end
103
+ end
104
+
105
+ def find_distinct_matches_for templates
106
+ templates.inject([]) do |tuples, template|
107
+ tuples << find_match_for(template, distinct_from: tuples)
108
+ end
109
+ end
110
+
111
+ def find_match_for template, distinct_from: []
112
+ case template
113
+ when OrderedEventTemplate
114
+ k, firsts = tree.first
115
+ k && k < template.expiration &&
116
+ firsts.find {|tuple| distinct_from.all? {|t| !t.equal? tuple}}
117
+ # The `find` clause isn't really needed, since OrderedEventTemplate is
118
+ # only used for reads, not takes, and anyway we never take multiple
119
+ # tuples in a transaction on the event space so the array would be empty.
120
+ # But let's be correct... Note the use of #equal?.
121
+ else
122
+ # Fall back to linear search, same as default tuplestore.
123
+ find do |tuple|
124
+ template === tuple and not distinct_from.any? {|t| t.equal? tuple}
125
+ end
126
+ end
127
+ end
128
+ end
@@ -7,29 +7,47 @@
7
7
  #
8
8
  # * the expiration manager need to sort by expiration time
9
9
  #
10
- # * the critical event alerter doesn't need to sort at all.
10
+ # * the critical event alerter doesn't need to sort or search at all,
11
+ # it just needs efficient insert and delete.
12
+ #
13
+ # Run with --http to expose a web API and run a test web client.
14
+ #
15
+ # You will need to `gem install rbtree sqlite3 sequel`.
16
+ # For the --http option, you'll also need to `gem install http json sinatra`.
11
17
 
12
- abort "work in progress"
18
+ USE_HTTP = ARGV.delete("--http")
19
+
20
+ if USE_HTTP
21
+ require_relative 'http-mode'
22
+ start_web_client
23
+ at_exit {Process.waitall}
24
+ end
13
25
 
14
26
  require 'tupelo/app'
15
- require_relative 'event-subspace'
16
- require_relative 'producer'
17
- require_relative 'expirer-v2'
27
+ require_relative '../event-subspace'
28
+ require_relative '../producer'
29
+ require_relative 'expirer'
18
30
 
19
31
  N_PRODUCERS = 3
20
32
  N_CONSUMERS = 2
21
33
 
22
34
  Tupelo.application do
23
-
24
35
  local do
25
- use_subspaces!
26
36
  define_event_subspace
27
37
  end
28
38
 
39
+ if USE_HTTP
40
+ # Web API using sinata to access the index of events.
41
+ child subscribe: "event" do |client|
42
+ log.progname = "web server"
43
+ run_web_server(client)
44
+ end
45
+ end
46
+
29
47
  N_PRODUCERS.times do |i|
30
48
  child subscribe: [] do # N.b., no subscriptions
31
49
  log.progname = "producer #{i}"
32
- run_producer i ### V2: manual tagging
50
+ run_producer i
33
51
  end
34
52
  end
35
53
 
@@ -38,13 +56,14 @@ Tupelo.application do
38
56
  child subscribe: "event", passive: true do ### tuplespace: sqlite
39
57
  log.progname = "consumer #{i}"
40
58
  read subspace("event") do |event|
41
- log event ### need filtering, actions, etc.
59
+ log.info event ### need filtering, actions, etc.
42
60
  end
43
61
  end
44
62
  end
45
63
 
46
64
  # critical event alerter
47
- child subscribe: "event", passive: true do ### tuplespace: bag?
65
+ require_relative 'hash-store'
66
+ child tuplespace: HashStore, subscribe: "event", passive: true do
48
67
  log.progname = "alerter"
49
68
  read Tupelo::Client::CRITICAL_EVENT do |event|
50
69
  log.error event
@@ -53,20 +72,16 @@ Tupelo.application do
53
72
 
54
73
  if argv.include?("--debug-expiration")
55
74
  # expired event debugger
75
+ require_relative '../expiration-dbg'
56
76
  child subscribe: "event", passive: true do
57
77
  log.progname = "expiration debugger"
58
- read Tupelo::Client::EXPIRED_EVENT do |event|
59
- log event
60
- end
78
+ run_expiration_debugger
61
79
  end
62
80
  end
63
81
 
64
82
  # expirer: stores current events and looks for events that can be expired.
65
- child subscribe: "event", passive: true do
83
+ child tuplespace: OrderedEventStore, subscribe: "event", passive: true do
66
84
  log.progname = "expirer"
67
85
  run_expirer_v2
68
- ### use rbtree
69
86
  end
70
-
71
- ### Add sinatra app.
72
87
  end
@@ -0,0 +1,160 @@
1
+ require 'sequel'
2
+
3
+ # Hard-coded to work with tuples belonging to the "poi" subspace
4
+ # and with the PoiStore table defined below.
5
+ class PoiTemplate
6
+ attr_reader :lat, :lng, :poispace
7
+
8
+ # lat and lng can be intervals or single values or nil to match any value
9
+ def initialize lat: nil, lng: nil, poispace: nil
10
+ @lat = lat
11
+ @lng = lng
12
+ @poispace = poispace
13
+ end
14
+
15
+ # we only need to define this method if we plan to wait for poi tuples
16
+ # locally using this template, i.e. read(template) or take(template)
17
+ def === tuple
18
+ @poispace === tuple and
19
+ !@lat || @lat === tuple["lat"] and
20
+ !@lng || @lng === tuple["lng"]
21
+ end
22
+
23
+ def find_in table, distinct_from: []
24
+ where_terms = {}
25
+ where_terms[:lat] = @lat if @lat
26
+ where_terms[:lng] = @lng if @lng
27
+
28
+ matches = table.
29
+ select(:lat, :lng, :desc).
30
+ where(where_terms).
31
+ limit(distinct_from.size + 1).all
32
+
33
+ distinct_from.each do |tuple|
34
+ if i=matches.index(tuple)
35
+ matches.delete_at i
36
+ end
37
+ end
38
+
39
+ matches.first
40
+ end
41
+ end
42
+
43
+ # A tuple store that is optimized for POI data. These tuples are stored in an
44
+ # in-memory sqlite database table. Tuples that do not fit this pattern (such
45
+ # as metatuples) are stored in an array, as in the default Tuplespace class.
46
+ class PoiStore
47
+ include Enumerable
48
+
49
+ attr_reader :table, :metas, :poispace
50
+
51
+ def self.define_poispace client
52
+ client.define_subspace("poi",
53
+ lat: Numeric,
54
+ lng: Numeric,
55
+ desc: String
56
+ )
57
+ client.subspace("poi") ## is this awkward?
58
+ end
59
+
60
+ # poispace should be client.subspace("poi"), but really we only need
61
+ # poispace.pot
62
+ def initialize poispace
63
+ @poispace = poispace
64
+ clear
65
+ end
66
+
67
+ def clear
68
+ @db = Sequel.sqlite
69
+ @db.create_table "poi" do
70
+ primary_key :id # id is not significant to our app
71
+ float :lat, null: false
72
+ float :lng, null: false
73
+ text :desc
74
+
75
+ ## spatial_index [:lat, :lng] # by default sqlite doesn't support this
76
+ index :lat
77
+ index :lng
78
+ end
79
+
80
+ @table = @db[:poi]
81
+ @metas = []
82
+ end
83
+
84
+ def each
85
+ table.each do |row|
86
+ yield "lat" => row[:lat], "lng" => row[:lng], "desc" => row[:desc]
87
+ end
88
+ metas.each do |tuple|
89
+ yield tuple
90
+ end
91
+ end
92
+
93
+ def insert tuple
94
+ case tuple
95
+ when poispace
96
+ table << tuple
97
+ else
98
+ metas << tuple
99
+ end
100
+ end
101
+
102
+ def delete_once tuple
103
+ case tuple
104
+ when poispace
105
+ id = table.select(:id).where(
106
+ lat: tuple["lat"],
107
+ lng: tuple["lng"],
108
+ desc: tuple["desc"]
109
+ ).limit(1)
110
+ count = table.where(id: id).delete
111
+
112
+ if count == 0
113
+ false
114
+ elsif count == 1
115
+ true
116
+ else
117
+ raise "internal error: primary key, id, was not unique"
118
+ end
119
+
120
+ else
121
+ if i=metas.index(tuple)
122
+ metas.delete_at i
123
+ end
124
+ end
125
+ end
126
+
127
+ def transaction inserts: [], deletes: [], tick: nil
128
+ deletes.each do |tuple|
129
+ delete_once tuple or raise "bug"
130
+ end
131
+
132
+ inserts.each do |tuple|
133
+ insert tuple.freeze ## should be deep_freeze
134
+ end
135
+ end
136
+
137
+ def find_distinct_matches_for templates
138
+ templates.inject([]) do |tuples, template|
139
+ tuples << find_match_for(template, distinct_from: tuples)
140
+ end
141
+ end
142
+
143
+ def find_match_for template, distinct_from: []
144
+ case template
145
+ when PoiTemplate
146
+ template.find_in table, distinct_from: distinct_from
147
+ else
148
+ ## if template can match subspace("poi")
149
+ # Fall back to linear search, same as default tuplestore.
150
+ find do |tuple|
151
+ template === tuple and not distinct_from.any? {|t| t.equal? tuple}
152
+ end
153
+ ## else
154
+ ## metas.find do |tuple|
155
+ ## template === tuple and not distinct_from.any? {|t| t.equal? tuple}
156
+ ## end
157
+ ## end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,58 @@
1
+ # POI -- Points Of Interest
2
+ #
3
+ # This example creates a sqlite db in memory with a table of locations and
4
+ # descriptions of points of interest, and attaches the db to a subspace of the
5
+ # tuplespace. The process which manages that subspace can now do two things:
6
+ #
7
+ # 1. accept inserts (via write)
8
+ #
9
+ # 2. custom queries, accessed by write to a different subspace
10
+ #
11
+ # You can have redundant instances of this, and that will distribute load
12
+ # in #2 above.
13
+ #
14
+ # See example/subspaces/addr-book.rb for more comments on the command/response
15
+ # subspace pattern.
16
+ #
17
+ # gem install sequel sqlite3
18
+
19
+ require 'tupelo/app'
20
+ require_relative 'poi-store'
21
+
22
+ Tupelo.application do
23
+ local do
24
+ POISPACE = PoiStore.define_poispace(self)
25
+ define_subspace("cmd", {id: nil, cmd: String, arg: nil})
26
+ define_subspace("rsp", {id: nil, result: nil})
27
+ end
28
+
29
+ child tuplespace: [PoiStore, POISPACE],
30
+ subscribe: ["poi", "cmd"], passive: true do
31
+ log.progname = "poi-store #{client_id}"
32
+
33
+ # handle custom queries here, using poi template
34
+ loop do
35
+ req = take subspace("cmd")
36
+ case req["cmd"]
37
+ when "find box"
38
+ arg = req["arg"] ## validate this
39
+ lat = arg["lat"]; lng = arg["lng"]
40
+ template = PoiTemplate.new(poispace: subspace("poi"),
41
+ lat: lat[0]..lat[1], lng: lng[0]..lng[1])
42
+ write id: req["id"], result: read_all(template)
43
+ end
44
+ end
45
+ end
46
+
47
+ child subscribe: "rsp" do
48
+ write lat: 12, lng: 34, desc: "foo"
49
+ write lat: 56, lng: 78, desc: "bar"
50
+ write lat: 12, lng: 34, desc: "foo" # dup is ok
51
+ write lat: 13, lng: 35, desc: "baz"
52
+
53
+ req_id = [client_id, 1]
54
+ write id: req_id, cmd: "find box", arg: {lat: [10, 14], lng: [30, 40]}
55
+ rsp = take id: req_id, result: nil
56
+ log rsp["result"]
57
+ end
58
+ end