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.
- checksums.yaml +4 -4
- data/README.md +98 -36
- data/bin/tup +1 -7
- data/bugs/take-write.rb +8 -0
- data/example/bingo/bingo-v2.rb +20 -0
- data/example/broker-queue.rb +35 -0
- data/example/child-of-child.rb +34 -0
- data/example/consistent-hash.rb +0 -2
- data/example/counters/lock.rb +24 -0
- data/example/counters/merge.rb +35 -0
- data/example/counters/optimistic.rb +29 -0
- data/example/dataflow.rb +21 -0
- data/example/dedup.rb +45 -0
- data/example/map-reduce/ex.rb +32 -0
- data/example/multi-tier/memo2.rb +0 -2
- data/example/pregel/dist-opt.rb +15 -0
- data/example/riemann/event-subspace.rb +2 -0
- data/example/riemann/expiration-dbg.rb +15 -0
- data/example/riemann/producer.rb +34 -13
- data/example/riemann/v1/expirer.rb +28 -0
- data/example/riemann/{riemann-v1.rb → v1/riemann.rb} +5 -8
- data/example/riemann/v2/expirer.rb +31 -0
- data/example/riemann/v2/hash-store.rb +33 -0
- data/example/riemann/v2/http-mode.rb +53 -0
- data/example/riemann/v2/ordered-event-store.rb +128 -0
- data/example/riemann/{riemann-v2.rb → v2/riemann.rb} +32 -17
- data/example/sqlite/poi-store.rb +160 -0
- data/example/sqlite/poi-v2.rb +58 -0
- data/example/sqlite/poi.rb +40 -0
- data/example/sqlite/tmp/poi-sqlite.rb +33 -0
- data/example/subspaces/addr-book-v1.rb +0 -2
- data/example/subspaces/addr-book-v2.rb +0 -2
- data/example/subspaces/addr-book.rb +0 -2
- data/example/subspaces/pubsub.rb +0 -2
- data/example/subspaces/ramp.rb +0 -2
- data/example/subspaces/shop/shop-v2.rb +0 -2
- data/example/subspaces/simple.rb +0 -1
- data/example/subspaces/sorted-set-space.rb +5 -0
- data/lib/tupelo/app.rb +8 -0
- data/lib/tupelo/archiver/persistent-tuplespace.rb +2 -2
- data/lib/tupelo/archiver/tuplespace.rb +2 -2
- data/lib/tupelo/client/reader.rb +18 -8
- data/lib/tupelo/client/subspace.rb +12 -4
- data/lib/tupelo/client/transaction.rb +13 -1
- data/lib/tupelo/client/worker.rb +27 -4
- data/lib/tupelo/client.rb +3 -5
- data/lib/tupelo/tuplets/persistent-archiver/tuplespace.rb +5 -0
- data/lib/tupelo/version.rb +1 -1
- data/test/lib/mock-client.rb +1 -0
- metadata +26 -7
- data/example/riemann/expirer-v1.rb +0 -25
data/example/riemann/producer.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
class Tupelo::Client
|
2
|
-
|
3
|
-
|
4
|
-
event = {
|
2
|
+
def base_event
|
3
|
+
@base_event ||= {
|
5
4
|
host: `hostname`.chomp,
|
6
|
-
service: "
|
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
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|