tupelo 0.16 → 0.17

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.
@@ -0,0 +1,73 @@
1
+ # TODO make more interesting by changing the set of workers over time.
2
+
3
+ require 'tupelo/app'
4
+ require 'tupelo/util/bin-circle'
5
+
6
+ N_BINS = 5
7
+ N_REPS = 30 # of each bin, to make distribution more uniform
8
+ N_ITER = 1000
9
+
10
+ Tupelo.application do
11
+ local do
12
+ use_subspaces!
13
+
14
+ N_BINS.times do |id|
15
+ define_subspace id, [id, Numeric, Numeric]
16
+ end
17
+
18
+ define_subspace "sum", ["sum", Numeric, Numeric, Numeric]
19
+ end
20
+
21
+ circle = BinCircle.new
22
+
23
+ N_BINS.times do |id|
24
+ circle.add_bin id, reps: N_REPS
25
+ end
26
+
27
+ N_BINS.times do |id|
28
+ child subscribe: [id], passive: true do
29
+ # take things belonging to the process's bin
30
+ count = 0
31
+ at_exit {log "load: #{count}"}
32
+
33
+ loop do
34
+ _, n1, n2 = take [id, Numeric, Numeric]
35
+ # could use take {|| ...} to optimistically start long computation
36
+ write ["sum", n1, n2, n1+n2]
37
+ count += 1
38
+ end
39
+
40
+ # the following loop is faster, but not horiz. scalable
41
+ if false
42
+ read [id, Numeric, Numeric] do |_, n1, n2|
43
+ write ["sum", n1, n2, n1+n2]
44
+ count += 1
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ local subscribe: ["sum"] do
51
+ srand(12345)
52
+
53
+ Thread.new do
54
+ N_ITER.times do |i|
55
+ ns = [rand(100), rand(100)]
56
+ bin_id = circle.find_bin(ns)
57
+ write [bin_id, *ns]
58
+ end
59
+ end
60
+
61
+ N_ITER.times do |i|
62
+ _, n1, n2, sum = take ["sum", Numeric, Numeric, Numeric]
63
+ unless n1 + n2 == sum
64
+ log.error "bad sum"
65
+ end
66
+ q,r = (100*(i+1)).divmod N_ITER
67
+ if r == 0
68
+ printf "\r%3d%", q
69
+ end
70
+ end
71
+ puts
72
+ end
73
+ end
@@ -1,6 +1,5 @@
1
- # Factor numbers using remote hosts. Run with --trace to see contention.
2
- # This is more "map" than "map-reduce", though you could aggregate the
3
- # factored numbers, such as by finding the largest prime factor.
1
+ # Modified prime-factor.rb attempts to balance load better. Improvement
2
+ # varies--typically around 50% faster with 12 remote hosts.
4
3
 
5
4
  require 'tupelo/app/remote'
6
5
 
@@ -9,28 +8,73 @@ hosts = hosts.split(",")
9
8
 
10
9
  Tupelo.tcp_application do
11
10
  hosts.each_with_index do |host, hi|
12
- remote host: host, passive: true, eval: %{
11
+ remote host: host, passive: true, log: true, eval: %{
13
12
  require 'prime' # ruby stdlib for prime factorization
14
13
  class M
15
- def initialize nh, hi
14
+ def initialize nh, hi, excl = []
16
15
  @nh, @hi = nh, hi
16
+ @excl = excl
17
17
  end
18
18
  def === x
19
19
  Array === x and
20
20
  x[0] == "input" and
21
- x[1] % @nh == @hi
21
+ x[1] % @nh == @hi and
22
+ not @excl.include? x[1]
23
+ end
24
+ def exclude *y
25
+ self.class.new @nh, @hi, @excl + y
22
26
  end
23
27
  end
24
28
  my_pref = M.new(#{hosts.size}, #{hi})
29
+
25
30
  loop do
26
- _, input =
31
+ txn = transaction
32
+ begin
33
+ _, input = txn.take_nowait(my_pref)
34
+ rescue TransactionFailure => ex
35
+ next
36
+ end
37
+
38
+ if input
27
39
  begin
28
- take(my_pref, timeout: 1.0) # fewer fails (5.0 -> none at all)
29
- rescue TimeoutError
30
- take(["input", Integer])
40
+ txn.commit
41
+ output = input.prime_division
42
+ Thread.new do
43
+ begin
44
+ txn.wait
45
+ rescue TransactionFailure
46
+ # someone else got it
47
+ else
48
+ write ["output", input, output]
49
+ end
50
+ end
51
+ rescue TransactionFailure
31
52
  end
53
+ my_pref = my_pref.exclude input
54
+ next
55
+ end
56
+
57
+ begin
58
+ txn.cancel
59
+ rescue TransactionFailure
60
+ end
61
+ break
62
+ end
63
+
64
+ loop do
65
+ _, input = take(["input", Integer])
32
66
  write ["output", input, input.prime_division]
33
67
  end
68
+
69
+
70
+ # _, input =
71
+ # begin
72
+ # take(my_pref, timeout: 1.0) # fewer fails (5.0 -> none at all)
73
+ # rescue TimeoutError
74
+ # take(["input", Integer])
75
+ # end
76
+ # write ["output", input, input.prime_division]
77
+ # end
34
78
  }
35
79
  end
36
80
 
@@ -4,7 +4,7 @@
4
4
  #
5
5
  # Unlike in a key-value store, a given key_string may occur more than once.
6
6
  # It is up to the application to decide whether to enforce key uniqueness or
7
- # not (for example, by taking (k,...) before writing (k,v).
7
+ # not (for example, by taking [k,...] before writing [k,v]).
8
8
  #
9
9
  # This store should be used only by clients that subscribe to a subspace
10
10
  # that can be represented as pairs. (See memo2.rb.)
@@ -16,14 +16,11 @@ fork do
16
16
  local do
17
17
  use_subspaces!
18
18
 
19
- define_subspace(
20
- tag: "memo",
21
- template: [
22
- {value: "memo"}, # tag is encoded in each tuple, for recognizing
23
- {type: "string"}, # key in the cache, must be string
24
- nil # value, can be any object (e.g. JSON object)
25
- ]
26
- )
19
+ define_subspace("memo", [
20
+ "memo", # tag is encoded in each tuple, for recognizing
21
+ String, # key in the cache, must be string
22
+ nil # value, can be any object (e.g. JSON object)
23
+ ])
27
24
  end
28
25
 
29
26
  child tuplespace: [KVSpace, "memo"], subscribe: ["memo"] do |client|
@@ -5,7 +5,7 @@
5
5
  #
6
6
  # Depends on the sinatra and http gems.
7
7
 
8
- PORTS = [9001, 9002 , 9003]
8
+ PORTS = [9001, 9002, 9003]
9
9
 
10
10
  fork do
11
11
  require 'tupelo/app'
@@ -35,43 +35,34 @@ Tupelo.application do
35
35
  use_subspaces!
36
36
 
37
37
  # Subspace for tuples belonging to the addr book.
38
- define_subspace(
39
- tag: ab_tag,
40
- template: [
41
- {value: ab_tag},
42
- {type: "string"}, # name
43
- nil # address; can be any object
44
- ]
45
- )
38
+ define_subspace(ab_tag, [
39
+ ab_tag,
40
+ String, # name
41
+ nil # address; can be any object
42
+ ])
46
43
 
47
44
  # Subspace for commands for fetch, delete, first, last, prev, next.
48
45
  # We can't use #read and #take for fetch and delete because then the
49
46
  # requesting client would have to subscribe to the ab_tag subspace.
50
- define_subspace(
51
- tag: cmd_tag,
52
- template: [
53
- {value: cmd_tag},
54
- nil, # request id, such as [client_id, uniq_id]
55
- {type: "string"}, # cmd name
56
- {type: "list"} # arguments
57
- ]
58
- )
47
+ define_subspace(cmd_tag, [
48
+ cmd_tag,
49
+ nil, # request id, such as [client_id, uniq_id]
50
+ String, # cmd name
51
+ Array # arguments
52
+ ])
59
53
 
60
54
  # Subspace for responses to commands. A response identifies the command
61
55
  # it is responding to in two ways: by copying it and by an id. The
62
56
  # former is so that another client can "spy" on one client's query
63
57
  # responses, perhaps saving effort. The latter is to distinguish between
64
58
  # iterations of the same command (first, first, ...).
65
- define_subspace(
66
- tag: resp_tag,
67
- template: [
68
- {value: resp_tag},
69
- nil, # in response to this request id
70
- {type: "string"}, # cmd name
71
- {type: "list"}, # arguments
72
- nil, # result of query -- type depends on command
73
- ]
74
- )
59
+ define_subspace(resp_tag, [
60
+ resp_tag,
61
+ nil, # in response to this request id
62
+ String, # cmd name
63
+ Array, # arguments
64
+ nil, # result of query -- type depends on command
65
+ ])
75
66
  end
76
67
 
77
68
  N_REPLICAS.times do |i|
@@ -19,22 +19,10 @@ Tupelo.application do
19
19
  use_subspaces!
20
20
 
21
21
  N_CHAN.times do |i|
22
- define_subspace(
23
- tag: i,
24
- template: [
25
- {value: i},
26
- {type: "string"}
27
- ]
28
- )
22
+ define_subspace i, [i, String]
29
23
  end
30
24
 
31
- define_subspace(
32
- tag: "control",
33
- template: [
34
- {value: "control"},
35
- nil
36
- ]
37
- )
25
+ define_subspace "control", ["control", nil]
38
26
  end
39
27
 
40
28
  N_PUBS.times do |pi|
@@ -45,31 +45,24 @@ Tupelo.application do
45
45
  local do
46
46
  use_subspaces!
47
47
 
48
- define_subspace(
49
- tag: "x",
50
- template: {
51
- x: {type: "number"}, # data payload
52
- id: {type: "list"}, # [client_id, local_id]
53
- final: {type: "boolean"} # false means pending
54
- }
55
- )
48
+ bool = PortableObjectTemplate::BOOLEAN
56
49
 
57
- define_subspace(
58
- tag: "y",
59
- template: {
60
- y: {type: "number"}, # data payload
61
- id: {type: "list"}, # [client_id, local_id]
62
- final: {type: "boolean"} # false means pending
63
- }
64
- )
65
-
66
- define_subspace(
67
- tag: "ack", # could make this per-client
68
- template: {
69
- ack: {type: "string"}, # state ack-ed: "pending"
70
- id: {type: "list"} # [client_id, local_id]
71
- }
72
- )
50
+ define_subspace("x", {
51
+ x: Numeric, # data payload
52
+ id: Array, # [client_id, local_id]
53
+ final: bool # false means pending
54
+ })
55
+
56
+ define_subspace("y", {
57
+ y: Numeric, # data payload
58
+ id: Array, # [client_id, local_id]
59
+ final: bool # false means pending
60
+ })
61
+
62
+ define_subspace("ack", { # could make this per-client
63
+ ack: String, # state ack-ed: "pending"
64
+ id: Array # [client_id, local_id]
65
+ })
73
66
  end
74
67
 
75
68
  X_REPLICATIONS.times do |xi|
@@ -12,14 +12,11 @@ Tupelo.application do
12
12
  local do
13
13
  use_subspaces!
14
14
 
15
- define_subspace(
16
- tag: "inventory",
17
- template: [
18
- {value: "product"},
19
- nil, # product_id
20
- {type: "number"} # count
21
- ]
22
- )
15
+ define_subspace("inventory", [
16
+ "product",
17
+ nil, # product_id
18
+ Integer # count
19
+ ])
23
20
 
24
21
  PRODUCT_IDS.each do |product_id|
25
22
  count = 10
@@ -9,15 +9,7 @@ Tupelo.application do
9
9
  log [subscribed_all, subscribed_tags]
10
10
 
11
11
  use_subspaces!
12
-
13
- define_subspace(
14
- tag: "foo",
15
- template: [
16
- {type: "number"}
17
- ]
18
- )
19
-
20
- write_wait [0]
12
+ define_subspace "foo", [Numeric]
21
13
 
22
14
  log read_all(Object)
23
15
  end
@@ -1,3 +1,7 @@
1
+ # This example doesn't work yet because there is no way to indicate that,
2
+ # instead of waiting, it would be better to continue searching the tuplespace,
3
+ # backtracking around the [Integer, String] tuple that did not have a mate.
4
+
1
5
  require 'tupelo/app'
2
6
 
3
7
  Tupelo.application do
File without changes
@@ -0,0 +1,59 @@
1
+ # This works, but requires a fix-up step to clean up after a race condition
2
+ # during counter initialization.
3
+
4
+ require 'tupelo/app'
5
+
6
+ Tupelo.application do
7
+ 2.times do
8
+ child passive: true do
9
+ loop do
10
+ transaction do
11
+ fish, _ = take([String])
12
+ n, _ = take_nowait([Integer, fish])
13
+ if n
14
+ write [n + 1, fish]
15
+ else
16
+ write [1, fish] # another process might also write this, so ...
17
+ write ["fixup", fish]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ child passive: true do
25
+ loop do
26
+ transaction do # fix up the two counter tuples
27
+ _, fish = take ["fixup", String]
28
+ n1, _ = take_nowait [Integer, fish]
29
+ if n1
30
+ n2, _ = take_nowait [Integer, fish]
31
+ if n2
32
+ #log "fixing: #{[n1 + n2, fish]}"
33
+ write [n1 + n2, fish]
34
+ else
35
+ write [n1, fish] # partial rollback
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ local do
43
+ seed = 3
44
+ srand seed
45
+ log "seed = #{seed}"
46
+
47
+ fishes = %w{ trout marlin char salmon }
48
+
49
+ a = fishes * 10
50
+ a.shuffle!
51
+ a.each do |fish|
52
+ write [fish]
53
+ end
54
+
55
+ fishes.each do |fish|
56
+ log take [10, fish]
57
+ end
58
+ end
59
+ end
@@ -68,6 +68,8 @@ module Tupelo
68
68
  # the passive flag for processes that wait for tuples and respond in some
69
69
  # way. Then you do not have to manually interrupt the whole application when
70
70
  # the active processes are done. See examples.
71
+ #
72
+ # Returns pid of child process.
71
73
  def child client_class = Client, passive: false, **opts, &block
72
74
  ez.child :seqd, :cseqd, :arcd, passive: passive do |seqd, cseqd, arcd|
73
75
  opts = {seq: seqd, cseq: cseqd, arc: arcd, log: log}.merge(opts)
@@ -0,0 +1,36 @@
1
+ class Tupelo::Client
2
+ module Api
3
+ def define_subspace tag, template, addr: nil
4
+ metatuple = {
5
+ __tupelo__: "subspace",
6
+ tag: tag,
7
+ template: PortableObjectTemplate.spec_from(template),
8
+ addr: addr
9
+ }
10
+ write_wait metatuple
11
+ end
12
+
13
+ # call this just once at start of first client (it's optional to
14
+ # preserve behavior of non-subspace-aware code)
15
+ def use_subspaces!
16
+ return if subspace(TUPELO_SUBSPACE_TAG)
17
+ define_subspace(TUPELO_SUBSPACE_TAG, {
18
+ __tupelo__: "subspace",
19
+ tag: nil,
20
+ template: nil,
21
+ addr: nil
22
+ })
23
+ end
24
+
25
+ def subspace tag
26
+ tag = tag.to_s
27
+ worker.subspaces.find {|sp| sp.tag == tag} or begin
28
+ if subscribed_tags.include? tag
29
+ read __tupelo__: "subspace", tag: tag, addr: nil, template: nil
30
+ worker.subspaces.find {|sp| sp.tag == tag}
31
+ end
32
+ end
33
+ ## this impl will not be safe with dynamic subspaces
34
+ end
35
+ end
36
+ end
@@ -71,8 +71,11 @@ class Tupelo::Client
71
71
  def take template, timeout: nil
72
72
  transaction timeout: timeout do |t|
73
73
  tuple = t.take template
74
- yield tuple if block_given?
75
- tuple
74
+ if block_given?
75
+ yield tuple
76
+ else
77
+ tuple
78
+ end
76
79
  end
77
80
  end
78
81
 
@@ -42,8 +42,8 @@ class Tupelo::Client
42
42
  def each(*); end
43
43
  def delete_once(*); end
44
44
  def insert(*); self; end
45
- def find_distinct_matches_for(*); raise; end ##?
46
- def find_match_for(*); raise; end ##?
45
+ def find_distinct_matches_for(*); raise; end
46
+ def find_match_for(*); raise; end
47
47
  def clear; end
48
48
 
49
49
  ## should store space metadata, so outgoing writes can be tagged
@@ -351,12 +351,9 @@ class Tupelo::Client
351
351
  end
352
352
 
353
353
  take_tuples.each do |tuple|
354
- ### abstract this out
355
- if tuple.kind_of? Hash and tuple.key? "__tupelo__"
356
- if tuple["__tupelo__"] == "subspace" # tuple is subspace metatdata
357
- ## do some error checking
358
- subspaces.delete_if {|sp| sp.tag == tuple["tag"]}
359
- end
354
+ if is_meta_tuple? tuple
355
+ ## do some error checking
356
+ subspaces.delete_if {|sp| sp.tag == tuple["tag"]}
360
357
  end
361
358
  end
362
359
  end
@@ -435,13 +432,17 @@ class Tupelo::Client
435
432
  end
436
433
  end
437
434
 
435
+ # Returns true if tuple is subspace metadata.
436
+ def is_meta_tuple? tuple
437
+ tuple.kind_of? Hash and tuple.key? "__tupelo__" and
438
+ tuple["__tupelo__"] == "subspace"
439
+ end
440
+
438
441
  def sniff_meta_tuple tuple
439
- if tuple.kind_of? Hash and tuple.key? "__tupelo__"
440
- if tuple["__tupelo__"] == "subspace" # tuple is subspace metatdata
441
- ## do some error checking
442
- ## what if subspace already exists?
443
- subspaces << Subspace.new(tuple, self)
444
- end
442
+ if is_meta_tuple? tuple
443
+ ## do some error checking
444
+ ## what if subspace already exists?
445
+ subspaces << Subspace.new(tuple, self)
445
446
  end
446
447
  end
447
448
 
data/lib/tupelo/client.rb CHANGED
@@ -4,6 +4,7 @@ module Tupelo
4
4
  class Client < Funl::Client
5
5
  require 'tupelo/client/worker'
6
6
  require 'tupelo/client/tuplespace'
7
+ require 'tupelo/client/subspace'
7
8
 
8
9
  include Api
9
10
 
@@ -60,37 +61,5 @@ module Tupelo
60
61
  super().unknown *args
61
62
  end
62
63
  end
63
-
64
- ## do these belong in API module?
65
- def define_subspace metatuple
66
- defaults = {__tupelo__: "subspace", addr: nil}
67
- write_wait defaults.merge!(metatuple)
68
- end
69
-
70
- # call this just once at start of first client (it's optional to
71
- # preserve behavior of non-subspace-aware code)
72
- def use_subspaces!
73
- return if subspace(TUPELO_SUBSPACE_TAG)
74
- define_subspace(
75
- tag: TUPELO_SUBSPACE_TAG,
76
- template: {
77
- __tupelo__: {value: "subspace"},
78
- tag: nil,
79
- addr: nil,
80
- template: nil
81
- }
82
- )
83
- end
84
-
85
- def subspace tag
86
- tag = tag.to_s
87
- worker.subspaces.find {|sp| sp.tag == tag} or begin
88
- if subscribed_tags.include? tag
89
- read __tupelo__: "subspace", tag: tag, addr: nil, template: nil
90
- worker.subspaces.find {|sp| sp.tag == tag}
91
- end
92
- end
93
- ## this impl will not be safe with dynamic subspaces
94
- end
95
64
  end
96
65
  end