tupelo 0.20 → 0.21

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -3
  3. data/bench/{tuplespace.rb → tuplestore.rb} +3 -3
  4. data/bin/tspy +2 -2
  5. data/bin/tup +20 -3
  6. data/example/multi-tier/{kvspace.rb → kvstore.rb} +2 -2
  7. data/example/multi-tier/memo2.rb +4 -4
  8. data/example/riemann/v2/event-sql.rb +56 -0
  9. data/example/riemann/v2/expirer.rb +4 -1
  10. data/example/riemann/v2/hash-store.rb +6 -6
  11. data/example/riemann/v2/ordered-event-store.rb +1 -1
  12. data/example/riemann/v2/riemann.rb +7 -4
  13. data/example/riemann/v2/sqlite-event-store.rb +174 -0
  14. data/example/sqlite/poi-client.rb +11 -0
  15. data/example/sqlite/poi-store.rb +38 -56
  16. data/example/sqlite/poi-template.rb +61 -0
  17. data/example/sqlite/poi-v2.rb +63 -33
  18. data/example/sqlite/poi.rb +13 -25
  19. data/example/sqlite/tmp/poi-sqlite.rb +10 -8
  20. data/example/subspaces/addr-book.rb +2 -2
  21. data/example/subspaces/{sorted-set-space.rb → sorted-set-store.rb} +4 -4
  22. data/example/wip/complex-tags.rb +45 -0
  23. data/lib/tupelo/app/trace.rb +1 -1
  24. data/lib/tupelo/app.rb +3 -3
  25. data/lib/tupelo/archiver/{persistent-tuplespace.rb → persistent-tuplestore.rb} +1 -1
  26. data/lib/tupelo/archiver/{tuplespace.rb → tuplestore.rb} +2 -2
  27. data/lib/tupelo/archiver/worker.rb +12 -12
  28. data/lib/tupelo/archiver.rb +7 -5
  29. data/lib/tupelo/client/reader.rb +13 -10
  30. data/lib/tupelo/client/subspace.rb +12 -4
  31. data/lib/tupelo/client/transaction.rb +98 -95
  32. data/lib/tupelo/client/{tuplespace.rb → tuplestore.rb} +4 -4
  33. data/lib/tupelo/client/worker.rb +44 -35
  34. data/lib/tupelo/client.rb +4 -4
  35. data/lib/tupelo/version.rb +1 -1
  36. data/test/lib/mock-client.rb +1 -1
  37. data/test/lib/testable-worker.rb +1 -1
  38. data/test/unit/test-ops.rb +1 -1
  39. metadata +13 -9
  40. data/example/map-reduce/ex.rb +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8f1def09de170ac1f78bdc5bfe99374b02566dab
4
- data.tar.gz: fe906b6b7f0724a056b93de2144b5b198b77c577
3
+ metadata.gz: 3ab41264ead7c8233c4e48f9f568ae68fb04e843
4
+ data.tar.gz: 01baffe87a234f70b3f2f84de84f38f62924d152
5
5
  SHA512:
6
- metadata.gz: b033a673de3a993f6cec2bdb76beca10797a37c579e6bba09c19073a1eea875e683d24cdd28ca2450378602f3ce74a8870ee14ede361ae35e5cf7e4a95fa460b
7
- data.tar.gz: 43e3999fe505e7eca3c483839e6dcbde880f9cdc7791bb2b88541ce43de9e506b47cc16354d595f051456146b45ea4a78422d9419b25e1e028eaa93a51cb716c
6
+ metadata.gz: 6808153d5b48baf8e376a72d4179ca20851da83833f938dfa00120da1ec20fe9f4fe504b4006bd0c88f243d9a445f6856ecc70af6013d27c1f13140b5946b67a
7
+ data.tar.gz: e57be7395a7d81c6cde4e3ec8c21f93761e76f1dc8c7d2f70a6331f59e563c5b7e0bd903119f2f86c2e161231f5cdfc4210144bdf223327c5f158d139caf5c2a
data/README.md CHANGED
@@ -113,17 +113,27 @@ Ssh is used to set up the remote processes. Additionally, with the `--tunnel` co
113
113
  Limitations
114
114
  ===========
115
115
 
116
- The main limitation of tupelo is that **all network communication passes through a single process**, the message sequencer. This process has minimal state and minimal computation. The state is just a counter and the network connections (no storage of tuples or other application data). The computation is just counter increment and message dispatch (no transaction execution or searches). A transaction requires just one message (possibly with many recipients) to pass through the sequencer. The message sequencer can be light and fast.
116
+ Bottleneck
117
+ ----------
118
+
119
+ The main limitation of tupelo is that, except for read-only operations, **all tuple operations pass through a single process**, the message sequencer.
120
+
121
+ The sequencer has minimal state and minimal computation. The state is just a counter and the network connections (no storage of tuples or other application data). The computation is just counter increment and message dispatch (no transaction execution or searches). A transaction requires just one message (possibly with many recipients) to pass through the sequencer. The message sequencer can be light and fast.
117
122
 
118
123
  Nevertheless, this process is a bottleneck. Each message traverses two hops, to and from the sequencer. Each tupelo client must be connected to the sequencer to transact on tuples (aside from local reads).
119
124
 
120
125
  **Tupelo will always have this limitation.** It is essential to the design of the system. By accepting this cost, we get some benefits, discussed in the next section.
121
126
 
127
+ Clients may communicate other data over side channels that do not go through the sequencer. For [example](example/socket-broker.rb), they can use the tuplespace to coordinate task assignments, data locations (perhaps external to the tuplespace), TCP hosts and ports, and other metadata, and then use direct connections for the data. The archiver, which is a special client that brings newly connected clients up to date, is another example of direct client-to-client connections.
128
+
129
+ Other limitations
130
+ -----------------
131
+
122
132
  The message sequencer is also a SPoF (single point of failure), but this is not inherent in the design. A future version of tupelo will have options for failover or clustering of the sequencer, perhaps based on [raft](http://raftconsensus.github.io), with a cost of increased latency and complexity. (However, redundancy and failover of *application* data and computation *is* supported by the current implementation; app data and computations are distributed among the client processes.)
123
133
 
124
134
  There are some limitations that may result from naive application of tupelo: high client memory use, high bandwidth use, high client cpu use. These resource issues can often be controlled with [subspaces](doc/subspace.md) and specialized data structures and data stores. There are several examples addressing these problems. Another approach is to use the tuplespace for low volume references to high volume data.
125
135
 
126
- Also, see the discussion in [transactions](doc/transactions.md) on limitations of transactions across subspaces.
136
+ Also, see the discussion in [transactions](doc/transactions.md) on limitations of transactions across subspaces. It's likely that these limitations will soon be lifted, at the cost of increased latency (only for cross-subspace transactions).
127
137
 
128
138
  This implementation is also limited in efficiency because of its use of Ruby.
129
139
 
@@ -149,7 +159,9 @@ As noted above, the sequencer assigns an incrementing sequence number, or *tick*
149
159
 
150
160
  * relatively easy data replication: all subscribers to a subspace replicate that subspace, possibly with different storage implementations;
151
161
 
152
- * the current state of the tuplespace can be computed from a earlier state by replaying the transactions in sequence;
162
+ * even though storage is distributed, the client programming model is that all tuples are in the same place at the same time; there is no need to reason about multiple clocks or clock skew;
163
+
164
+ * the current state of the tuplespace can be computed from an earlier state by replaying the transactions in sequence;
153
165
 
154
166
  * the evolution of system state over time is observable, and tupelo provides the tools to do so: the `--trace` switch, the `#trace` api, and the `tspy` program.
155
167
 
@@ -1,17 +1,17 @@
1
- # Benchmark the default tuplespace (which is not intended to be fast).
1
+ # Benchmark the default tuplestore (which is not intended to be fast).
2
2
 
3
3
  module Tupelo
4
4
  class Client; end
5
5
  end
6
6
 
7
- require 'tupelo/client/tuplespace'
7
+ require 'tupelo/client/tuplestore'
8
8
  require 'benchmark'
9
9
 
10
10
  N_TUPLES = 100_000
11
11
  N_DELETES = 10_000
12
12
 
13
13
  Benchmark.bm(20) do |b|
14
- ts = Tupelo::Client::SimpleTuplespace.new
14
+ ts = Tupelo::Client::SimpleTupleStore.new
15
15
 
16
16
  b.report('insert') do
17
17
  N_TUPLES.times do |i|
data/bin/tspy CHANGED
@@ -28,12 +28,12 @@ if ARGV.delete("-h") or ARGV.delete("--help")
28
28
  end
29
29
 
30
30
  require 'tupelo/app'
31
- require 'tupelo/archiver/tuplespace'
31
+ require 'tupelo/archiver/tuplestore'
32
32
 
33
33
  Tupelo.application do
34
34
  # Use hash-and-count-based storage, for efficiency (this client never
35
35
  # does take or read).
36
- local tuplespace: [Tupelo::Archiver::Tuplespace, zero_tolerance: 1000] do
36
+ local tuplestore: [Tupelo::Archiver::TupleStore, zero_tolerance: 1000] do
37
37
  trap :INT do
38
38
  exit!
39
39
  end
data/bin/tup CHANGED
@@ -61,9 +61,14 @@ if ARGV.delete("-h") or ARGV.delete("--help")
61
61
  --yaml
62
62
  --json
63
63
  --msgpack <-- default
64
+
65
+ --symbol-keys
66
+ --string-keys
67
+ for json and msgpack, represent hash keys as symbols
68
+ or strings (see doc/faq for more details)
64
69
 
65
70
  --persist-dir DIR
66
- load and save tuplespace to DIR
71
+ load and save tuplestore to DIR
67
72
  (only needs to be set on first tup invocation)
68
73
 
69
74
  --subscribe TAG,TAG,...
@@ -78,7 +83,10 @@ require 'tupelo/app'
78
83
 
79
84
  argv, tupelo_opts = Tupelo.parse_args(ARGV)
80
85
 
81
- pubsub = argv.delete("--pubsub") # not a standard tupelo opt
86
+ # non-standard tupelo opts:
87
+ pubsub = argv.delete("--pubsub")
88
+ symbol_keys = argv.delete("--symbol-keys")
89
+ string_keys = argv.delete("--string-keys")
82
90
 
83
91
  if i=argv.index("--subscribe") # default is to subscribe to all
84
92
  argv.delete("--subscribe")
@@ -142,9 +150,18 @@ Tupelo.application(
142
150
  end
143
151
 
144
152
  client_opts = {}
153
+
154
+ if string_keys
155
+ client_opts[:symbolize_keys] = false
156
+ end
157
+
158
+ if symbol_keys
159
+ client_opts[:symbolize_keys] = true
160
+ end
161
+
145
162
  if pubsub
146
163
  client_opts[:arc] = nil
147
- client_opts[:tuplespace] = TupClient::NullTuplespace
164
+ client_opts[:tuplestore] = TupClient::NullTupleStore
148
165
  end
149
166
 
150
167
  if subscribed_tags
@@ -10,8 +10,8 @@
10
10
  # that can be represented as pairs. (See memo2.rb.)
11
11
  #
12
12
  # This store also manages meta tuples, which it keeps in an array, just like
13
- # the default Tuplespace class does.
14
- class KVSpace
13
+ # the default TupleStore class does.
14
+ class KVStore
15
15
  include Enumerable
16
16
 
17
17
  attr_reader :tag, :hash, :metas
@@ -1,7 +1,7 @@
1
- # Better, but more complex, implementation of memo.rb. Uses a custom tuplespace
1
+ # Better, but more complex, implementation of memo.rb. Uses a custom tuplestore
2
2
  # that is optimized for storing key-value data, rather than general tuples.
3
3
  # Also, subscribes to just the relevant subspace. Consequently, this example
4
- # should scale up to large memo spaces much better than memo.rb, which uses
4
+ # should scale up to large memo stores much better than memo.rb, which uses
5
5
  # linear search.
6
6
  #
7
7
  # Depends on the sinatra, json, and http gems.
@@ -10,7 +10,7 @@ require 'json'
10
10
 
11
11
  fork do
12
12
  require 'tupelo/app'
13
- require_relative 'kvspace.rb'
13
+ require_relative 'kvstore'
14
14
 
15
15
  Tupelo.application do
16
16
  local do
@@ -21,7 +21,7 @@ fork do
21
21
  ])
22
22
  end
23
23
 
24
- child tuplespace: [KVSpace, "memo"], subscribe: ["memo"] do |client|
24
+ child tuplestore: [KVStore, "memo"], subscribe: ["memo"] do |client|
25
25
  require 'sinatra/base'
26
26
 
27
27
  Class.new(Sinatra::Base).class_eval do
@@ -0,0 +1,56 @@
1
+ require 'sequel'
2
+
3
+ @db = Sequel.sqlite
4
+ @db.create_table :events do
5
+ primary_key :id # id is not significant to our app
6
+ text :host, null: false ## need this ?
7
+ text :service, null: false
8
+ text :state
9
+ number :time
10
+ text :description
11
+ number :metric
12
+ number :ttl
13
+
14
+ index [:host, :service]
15
+ end
16
+
17
+ @db.create_table :tags do
18
+ foreign_key :event_id, :events
19
+ text :tag
20
+ index :tag
21
+ primary_key [:event_id, :tag]
22
+ end
23
+
24
+ @db.create_table :customs do
25
+ foreign_key :event_id, :events
26
+ text :key
27
+ text :value
28
+ index :key
29
+ primary_key [:event_id, :key]
30
+ end
31
+
32
+ require 'pp'
33
+ #@db.tables.each do |table|
34
+ # pp @db.schema(table)
35
+ #end
36
+
37
+ events = @db[:events]
38
+ tags = @db[:tags]
39
+ customs = @db[:customs]
40
+
41
+ events << {host: "foo.bar", service: "httpd"}
42
+ events << {host: "foo.bar", service: "sshd"}
43
+ tags << {event_id: 1, tag: "red"}
44
+ customs << {event_id: 1, key: "a", value: "1"}
45
+ customs << {event_id: 1, key: "b", value: "2"}
46
+ customs << {event_id: 2, key: "b", value: "3"}
47
+
48
+ pp events.all
49
+ pp tags.all
50
+ pp customs.all
51
+
52
+ full_events = events.join(:tags, :event_id => :id).join(:customs, :event_id => :events__id)
53
+
54
+ p full_events
55
+ pp full_events.all
56
+
@@ -5,7 +5,7 @@ class Tupelo::Client
5
5
  #
6
6
  # A little more complex than v1, but more efficient.
7
7
  #
8
- # This version uses the rbtree to manage the space itself (in the worker
8
+ # This version uses the rbtree to manage the tuplestore itself (in the worker
9
9
  # thread), instead of using an rbtree to manage the scheduler (in the client
10
10
  # thread). It also uses a custom template class to perform range-based queries
11
11
  # of the rbtree key, which is not explicitly stored in the tuples. The rbree
@@ -15,6 +15,9 @@ class Tupelo::Client
15
15
  def run_expirer_v2
16
16
  loop do
17
17
  event = read subspace("event") # event with lowest time+ttl
18
+ # This just happens to be true, but alternately we could define
19
+ # OrderedEventTemplate.frist() to pass in a template that explicitly
20
+ # finds the lowest key.
18
21
  dt_next_expiration = event["time"] + event["ttl"] - Time.now.to_f
19
22
  begin
20
23
  transaction timeout: dt_next_expiration do
@@ -1,9 +1,9 @@
1
- require 'tupelo/archiver/tuplespace'
1
+ require 'tupelo/archiver/tuplestore'
2
2
 
3
3
  # Store for any kind of tuple, and faster for lookups of literal tuples,
4
4
  # rather than matching with templates or other queries.
5
- # See also example/multitier/kvspace.rb.
6
- class HashStore < Tupelo::Archiver::Tuplespace
5
+ # See also example/multitier/kvstore.rb.
6
+ class HashStore < Tupelo::Archiver::TupleStore
7
7
  def initialize zero_tolerance: 1000
8
8
  super
9
9
  end
@@ -14,11 +14,11 @@ class HashStore < Tupelo::Archiver::Tuplespace
14
14
  super
15
15
 
16
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
17
+ # We added this case to Archiver::TupleStore just so the read(..)
18
+ # will work correctly on tuples that are already in the store
19
19
  # when this process starts up. After that point, incoming tuples
20
20
  # are matched directly against the CRITICAL_EVENT template without
21
- # searching the space.
21
+ # searching the store.
22
22
 
23
23
  # We're not going to use Client#take in this client, so there's no need
24
24
  # to handle the distinct_from keyword argument.
@@ -37,7 +37,7 @@ end
37
37
  # other spaces/stores without modification.
38
38
  #
39
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
40
+ # just like the default TupleStore class does. Actually, any tuple for which
41
41
  # `tuple["time"] + tuple["ttl"]` raises an exception will go in the metas,
42
42
  # but in this example the process only subscribes to events and metas.
43
43
  #
@@ -27,6 +27,8 @@ require 'tupelo/app'
27
27
  require_relative '../event-subspace'
28
28
  require_relative '../producer'
29
29
  require_relative 'expirer'
30
+ require_relative 'hash-store'
31
+ require_relative 'sqlite-event-store'
30
32
 
31
33
  N_PRODUCERS = 3
32
34
  N_CONSUMERS = 2
@@ -34,6 +36,7 @@ N_CONSUMERS = 2
34
36
  Tupelo.application do
35
37
  local do
36
38
  define_event_subspace
39
+ EVENT_SPACE = client.subspace("event")
37
40
  end
38
41
 
39
42
  if USE_HTTP
@@ -53,7 +56,8 @@ Tupelo.application do
53
56
 
54
57
  N_CONSUMERS.times do |i|
55
58
  # stores events indexed by host, service
56
- child subscribe: "event", passive: true do ### tuplespace: sqlite
59
+ child tuplestore: [SqliteEventStore, EVENT_SPACE],
60
+ subscribe: "event", passive: true do
57
61
  log.progname = "consumer #{i}"
58
62
  read subspace("event") do |event|
59
63
  log.info event ### need filtering, actions, etc.
@@ -62,8 +66,7 @@ Tupelo.application do
62
66
  end
63
67
 
64
68
  # critical event alerter
65
- require_relative 'hash-store'
66
- child tuplespace: HashStore, subscribe: "event", passive: true do
69
+ child tuplestore: HashStore, subscribe: "event", passive: true do
67
70
  log.progname = "alerter"
68
71
  read Tupelo::Client::CRITICAL_EVENT do |event|
69
72
  log.error event
@@ -80,7 +83,7 @@ Tupelo.application do
80
83
  end
81
84
 
82
85
  # expirer: stores current events and looks for events that can be expired.
83
- child tuplespace: OrderedEventStore, subscribe: "event", passive: true do
86
+ child tuplestore: OrderedEventStore, subscribe: "event", passive: true do
84
87
  log.progname = "expirer"
85
88
  run_expirer_v2
86
89
  end
@@ -0,0 +1,174 @@
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
47
+
48
+ # A tuple store that is optimized for event data. These tuples are stored in an
49
+ # in-memory sqlite database table. Tuples that do not fit this pattern (such
50
+ # as metatuples) are stored in an array, as in the default TupleStore class.
51
+ class SqliteEventStore
52
+ include Enumerable
53
+
54
+ attr_reader :events, :metas, :space
55
+
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
+ clear
61
+ end
62
+
63
+ def clear
64
+ @db = Sequel.sqlite
65
+ @db.create_table :events do
66
+ primary_key :id # id is not significant to our app
67
+ text :host, null: false ## need this ?
68
+ text :service, null: false
69
+ text :state
70
+ number :time
71
+ text :description
72
+ number :metric
73
+ number :ttl
74
+
75
+ index [:host, :service]
76
+ end
77
+
78
+ @db.create_table :tags do
79
+ foreign_key :event_id, :events
80
+ text :tag
81
+ index :tag
82
+ primary_key [:event_id, :tag]
83
+ end
84
+
85
+ @db.create_table :customs do
86
+ foreign_key :event_id, :events
87
+ text :key
88
+ text :value
89
+ index :key
90
+ primary_key [:event_id, :key]
91
+ end
92
+
93
+ @events = @db[:events]
94
+ @metas = []
95
+ end
96
+
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
106
+ end
107
+ end
108
+
109
+ def insert tuple
110
+ case tuple
111
+ when space
112
+ events << tuple ## minus tags and customs
113
+ else
114
+ metas << tuple
115
+ end
116
+ end
117
+
118
+ def delete_once tuple
119
+ 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
125
+
126
+ if count == 0
127
+ false
128
+ elsif count == 1
129
+ true
130
+ else
131
+ raise "internal error: primary key, id, was not unique"
132
+ end
133
+
134
+ else
135
+ if i=metas.index(tuple)
136
+ metas.delete_at i
137
+ end
138
+ end
139
+ end
140
+
141
+ def transaction inserts: [], deletes: [], tick: nil
142
+ deletes.each do |tuple|
143
+ delete_once tuple or raise "bug"
144
+ end
145
+
146
+ inserts.each do |tuple|
147
+ insert tuple.freeze ## should be deep_freeze
148
+ end
149
+ end
150
+
151
+ def find_distinct_matches_for templates
152
+ templates.inject([]) do |tuples, template|
153
+ tuples << find_match_for(template, distinct_from: tuples)
154
+ end
155
+ end
156
+
157
+ def find_match_for template, distinct_from: []
158
+ case template
159
+ when EventTemplate
160
+ template.find_in events, distinct_from: distinct_from
161
+ else
162
+ ## if template can match subspace
163
+ # Fall back to linear search, same as default tuplestore.
164
+ find do |tuple|
165
+ template === tuple and not distinct_from.any? {|t| t.equal? tuple}
166
+ end
167
+ ## else
168
+ ## metas.find do |tuple|
169
+ ## template === tuple and not distinct_from.any? {|t| t.equal? tuple}
170
+ ## end
171
+ ## end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,11 @@
1
+ require 'tupelo/client'
2
+ require_relative 'poi-store'
3
+
4
+ class PoiClient < Tupelo::Client
5
+ def initialize *args, poispace: nil, subscribe: [], **opts
6
+ super *args, **opts,
7
+ tuplestore: [PoiStore, poispace.spec],
8
+ subscribe: [poispace.tag] + [subscribe].flatten,
9
+ symbolize_keys: true # for ease of use with sequel DB interface
10
+ end
11
+ end
@@ -1,52 +1,16 @@
1
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
2
+ require_relative 'poi-template'
42
3
 
43
4
  # A tuple store that is optimized for POI data. These tuples are stored in an
44
5
  # 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.
6
+ # as metatuples) are stored in an array, as in the default TupleStore class.
46
7
  class PoiStore
47
8
  include Enumerable
48
9
 
49
- attr_reader :table, :metas, :poispace
10
+ attr_reader :table, :metas
11
+
12
+ # Template for matching all POI tuples.
13
+ attr_reader :poi_template
50
14
 
51
15
  def self.define_poispace client
52
16
  client.define_subspace("poi",
@@ -54,14 +18,24 @@ class PoiStore
54
18
  lng: Numeric,
55
19
  desc: String
56
20
  )
57
- client.subspace("poi") ## is this awkward?
21
+ client.subspace("poi")
22
+ # this waits for ack of write of subspace metatuple, and then
23
+ # it returns the Subspace object, which contains a tag and a template
24
+ # spec, from which we can later construct a correct template in
25
+ # initialize.
58
26
  end
59
27
 
60
- # poispace should be client.subspace("poi"), but really we only need
61
- # poispace.pot
62
- def initialize poispace
63
- @poispace = poispace
28
+ def initialize spec, client: nil
29
+ @poi_template = client.worker.pot_for(spec)
30
+ # calling #pot_for in client means that resulting template
31
+ # will have keys converted as needed (in the case of this client,
32
+ # to symbols).
33
+
64
34
  clear
35
+
36
+ # To be more general, we could inspect the spec to determine which keys to
37
+ # use when creating and querying the table, and in the PoiTemplate class
38
+ # above. This example just assumes the key names are always lat, lng, desc.
65
39
  end
66
40
 
67
41
  def clear
@@ -82,8 +56,8 @@ class PoiStore
82
56
  end
83
57
 
84
58
  def each
85
- table.each do |row|
86
- yield "lat" => row[:lat], "lng" => row[:lng], "desc" => row[:desc]
59
+ table.select(:lat, :lng, :desc).each do |row|
60
+ yield row
87
61
  end
88
62
  metas.each do |tuple|
89
63
  yield tuple
@@ -92,7 +66,7 @@ class PoiStore
92
66
 
93
67
  def insert tuple
94
68
  case tuple
95
- when poispace
69
+ when poi_template
96
70
  table << tuple
97
71
  else
98
72
  metas << tuple
@@ -101,12 +75,10 @@ class PoiStore
101
75
 
102
76
  def delete_once tuple
103
77
  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)
78
+ when poi_template
79
+ id = table.select(:id).
80
+ where(tuple).
81
+ limit(1)
110
82
  count = table.where(id: id).delete
111
83
 
112
84
  if count == 0
@@ -157,4 +129,14 @@ class PoiStore
157
129
  ## end
158
130
  end
159
131
  end
132
+
133
+ def find_all_matches_for template, &bl
134
+ case template
135
+ when PoiTemplate
136
+ template.find_all_in table, &bl
137
+ else
138
+ # Fall back to linear search using #each and #===.
139
+ grep template, &bl
140
+ end
141
+ end
160
142
  end