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.
- checksums.yaml +4 -4
- data/README.md +15 -3
- data/bench/{tuplespace.rb → tuplestore.rb} +3 -3
- data/bin/tspy +2 -2
- data/bin/tup +20 -3
- data/example/multi-tier/{kvspace.rb → kvstore.rb} +2 -2
- data/example/multi-tier/memo2.rb +4 -4
- data/example/riemann/v2/event-sql.rb +56 -0
- data/example/riemann/v2/expirer.rb +4 -1
- data/example/riemann/v2/hash-store.rb +6 -6
- data/example/riemann/v2/ordered-event-store.rb +1 -1
- data/example/riemann/v2/riemann.rb +7 -4
- data/example/riemann/v2/sqlite-event-store.rb +174 -0
- data/example/sqlite/poi-client.rb +11 -0
- data/example/sqlite/poi-store.rb +38 -56
- data/example/sqlite/poi-template.rb +61 -0
- data/example/sqlite/poi-v2.rb +63 -33
- data/example/sqlite/poi.rb +13 -25
- data/example/sqlite/tmp/poi-sqlite.rb +10 -8
- data/example/subspaces/addr-book.rb +2 -2
- data/example/subspaces/{sorted-set-space.rb → sorted-set-store.rb} +4 -4
- data/example/wip/complex-tags.rb +45 -0
- data/lib/tupelo/app/trace.rb +1 -1
- data/lib/tupelo/app.rb +3 -3
- data/lib/tupelo/archiver/{persistent-tuplespace.rb → persistent-tuplestore.rb} +1 -1
- data/lib/tupelo/archiver/{tuplespace.rb → tuplestore.rb} +2 -2
- data/lib/tupelo/archiver/worker.rb +12 -12
- data/lib/tupelo/archiver.rb +7 -5
- data/lib/tupelo/client/reader.rb +13 -10
- data/lib/tupelo/client/subspace.rb +12 -4
- data/lib/tupelo/client/transaction.rb +98 -95
- data/lib/tupelo/client/{tuplespace.rb → tuplestore.rb} +4 -4
- data/lib/tupelo/client/worker.rb +44 -35
- data/lib/tupelo/client.rb +4 -4
- data/lib/tupelo/version.rb +1 -1
- data/test/lib/mock-client.rb +1 -1
- data/test/lib/testable-worker.rb +1 -1
- data/test/unit/test-ops.rb +1 -1
- metadata +13 -9
- data/example/map-reduce/ex.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ab41264ead7c8233c4e48f9f568ae68fb04e843
|
4
|
+
data.tar.gz: 01baffe87a234f70b3f2f84de84f38f62924d152
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
*
|
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
|
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/
|
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::
|
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/
|
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
|
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
|
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
|
-
|
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[:
|
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
|
14
|
-
class
|
13
|
+
# the default TupleStore class does.
|
14
|
+
class KVStore
|
15
15
|
include Enumerable
|
16
16
|
|
17
17
|
attr_reader :tag, :hash, :metas
|
data/example/multi-tier/memo2.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
# Better, but more complex, implementation of memo.rb. Uses a custom
|
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
|
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 '
|
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
|
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
|
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/
|
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/
|
6
|
-
class HashStore < Tupelo::Archiver::
|
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::
|
18
|
-
# will work correctly on tuples that are already in the
|
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
|
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
|
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
|
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
|
-
|
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
|
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
|
data/example/sqlite/poi-store.rb
CHANGED
@@ -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
|
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
|
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")
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
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
|
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
|
105
|
-
id = table.select(:id).
|
106
|
-
|
107
|
-
|
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
|