bud 0.9.4 → 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -66,9 +66,11 @@ Second, note that our Bud program's one statement merges the values on its right
66
66
  ### Tables and Scratches ###
67
67
  Before we dive into writing server code, let's try a slightly more involved single-timestep example. Start up rebl again, and paste in the following:
68
68
 
69
- table :clouds
70
- clouds <= [[1, "Cirrus"], [2, "Cumulus"]]
71
- stdio <~ clouds.inspected
69
+ ``` ruby
70
+ table :clouds
71
+ clouds <= [[1, "Cirrus"], [2, "Cumulus"]]
72
+ stdio <~ clouds.inspected
73
+ ```
72
74
 
73
75
  Now tick your rebl, but don't quit yet.
74
76
 
@@ -128,8 +130,10 @@ Now that we've seen a bit of Bloom, we're ready to write our first interesting s
128
130
 
129
131
  Even though we're getting ahead of ourselves, let's have a peek at the Bloom statements that implement the server in `examples/chat/chat_server.rb`:
130
132
 
131
- nodelist <= signup.payloads
132
- mcast <~ (mcast * nodelist).pairs { |m,n| [n.key, m.val] }
133
+ ``` ruby
134
+ nodelist <= connect { |c| [c.client, c.nick] }
135
+ mcast <~ (mcast * nodelist).pairs { |m,n| [n.key, m.val] }
136
+ ```
133
137
 
134
138
  That's it! There is one statement for each of the two sentences describing the behavior of the "basic idea" above. We'll go through these two statements in more detail shortly. But it's nice to see right away how concisely and naturally a Bloom program can fit our intuitive description of a distributed service.
135
139
 
@@ -137,14 +141,16 @@ That's it! There is one statement for each of the two sentences describing the
137
141
 
138
142
  Now that we've satisfied our need to peek, let's take this a bit more methodically. First we need declarations for the various Bloom collections we'll be using. We put the declarations that are common to both client and server into file `examples/chat/chat_protocol.rb`:
139
143
 
140
- module ChatProtocol
141
- state do
142
- channel :mcast
143
- channel :connect
144
- end
145
-
146
- DEFAULT_ADDR = "localhost:12345"
147
- end
144
+ ``` ruby
145
+ module ChatProtocol
146
+ state do
147
+ channel :connect, [:@addr, :client] => [:nick]
148
+ channel :mcast
149
+ end
150
+
151
+ DEFAULT_ADDR = "localhost:12345"
152
+ end
153
+ ```
148
154
 
149
155
  This defines a [Ruby mixin module](http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_modules.html) called `ChatProtocol` that has a couple special Bloom features:
150
156
 
@@ -156,33 +162,34 @@ This defines a [Ruby mixin module](http://www.ruby-doc.org/docs/ProgrammingRuby/
156
162
 
157
163
  Given this protocol (and the Ruby constant at the bottom), we're now ready to examine `examples/chat/chat_server.rb` in more detail:
158
164
 
159
- require 'rubygems'
160
- require 'bud'
161
- require 'chat_protocol'
165
+ ``` ruby
166
+ require 'rubygems'
167
+ require 'bud'
168
+ require_relative 'chat_protocol'
162
169
 
163
- class ChatServer
164
- include Bud
165
- include ChatProtocol
170
+ class ChatServer
171
+ include Bud
172
+ include ChatProtocol
166
173
 
167
- state { table :nodelist }
168
-
169
- bloom do
170
- nodelist <= connect.payloads
171
- mcast <~ (mcast * nodelist).pairs { |m,n| [n.key, m.val] }
172
- end
173
- end
174
-
175
- if ARGV.first
176
- addr = ARGV.first
177
- else
178
- addr = ChatProtocol::DEFAULT_ADDR
179
- end
180
-
181
- ip, port = addr.split(":")
182
- puts "Server address: #{ip}:#{port}"
183
- program = ChatServer.new(:ip => ip, :port => port.to_i)
184
- program.run
174
+ state { table :nodelist }
185
175
 
176
+ bloom do
177
+ nodelist <= connect { |c| [c.client, c.nick] }
178
+ mcast <~ (mcast * nodelist).pairs { |m,n| [n.key, m.val] }
179
+ end
180
+ end
181
+
182
+ if ARGV.first
183
+ addr = ARGV.first
184
+ else
185
+ addr = ChatProtocol::DEFAULT_ADDR
186
+ end
187
+
188
+ ip, port = addr.split(":")
189
+ puts "Server address: #{ip}:#{port}"
190
+ program = ChatServer.new(:ip => ip, :port => port.to_i)
191
+ program.run_fg
192
+ ```
186
193
 
187
194
  The first few lines get the appropriate Ruby classes and modules loaded via `require`. We then define the ChatServer class which mixes in the `Bud` module and the ChatProtocol module we looked at above. Then we have another `state` block that declares one additional collection, the `nodelist` table.
188
195
 
@@ -190,14 +197,18 @@ With those preliminaries aside, we have our first `bloom` block, which is how Bl
190
197
 
191
198
  The first is pretty simple:
192
199
 
193
- nodelist <= connect.payloads
200
+ ``` ruby
201
+ nodelist <= connect { |c| [c.client, c.nick] }
202
+ ```
194
203
 
195
- This says that whenever messages arrive on the channel named "connect", their payloads (i.e. their non-address field) should be instantaneously merged into the table nodelist, which will store them persistently. Note that nodelist has a \[key/val\] pair structure, so we expect the payloads will have that structure as well.
204
+ This says that whenever messages arrive on the channel named "connect", the client address and user-provided nickname should be instantaneously merged into the table "nodelist", which will store them persistently. Note that nodelist has a \[key/val\] pair structure, so it is suitable for storing pairs of (IP address, nickname).
196
205
 
197
- The next Bloom statement is more complex. Remember the description in the "basic idea" at the beginning of this section: the server needs to accept inbound chat messages from clients, and forward them to other clients.
206
+ The next Bloom statement is more complex. Remember the description in the "basic idea" at the beginning of this section: the server needs to accept inbound chat messages from clients and forward them to other clients.
207
+
208
+ ``` ruby
209
+ mcast <~ (mcast * nodelist).pairs { |m,n| [n.key, m.val] }
210
+ ```
198
211
 
199
- mcast <~ (mcast * nodelist).pairs { |m,n| [n.key, m.val] }
200
-
201
212
  The first thing to note is the lhs and operator in this statement. We are merging items (asynchronously, of course!) into the mcast channel, where they will be sent to their eventual destination.
202
213
 
203
214
  The rhs is our first introduction to the `*` operator of Bloom collections, and the `pairs` method after it. You can think of the `*` operator as "all-pairs": it produces a Bloom collection containing all pairs of mcast and nodelist items. The `pairs` method iterates through these pairs, passing them through a code block via the block arguments `m` and `n`. Finally, for each such pair the block produces an item containing the `key` attribute of the nodelist item, and the `val` attribute of the mcast item. This is structured as a proper \[address, val\] entry to be merged back into the mcast channel. Putting this together, this statement *multicasts inbound payloads on the mcast channel to all nodes in the chat*.
@@ -205,7 +216,7 @@ The rhs is our first introduction to the `*` operator of Bloom collections, and
205
216
  The remaining lines of plain Ruby simply instantiate and run the ChatServer class (which includes the `Bud` module) using an ip and port given on the command line (or the default from ChatProtocol.rb).
206
217
 
207
218
  #### `*`'s and Clouds ####
208
- You can think of out use of the `*` operator in the rhs of the second statement in a few different ways:
219
+ You can think of our use of the `*` operator on the rhs of the second statement in a few different ways:
209
220
 
210
221
  * If you're familiar with event-loop programming, this implements an *event handler* for messages on the mcast channel: whenever an mcast message arrives, this handler performs lookups in the nodelist table to form new messages. (It is easy to add "filters" to these handlers as arguments to `pairs`.) The resulting messages are dispatched via the mcast channel accordingly. This is a very common pattern in Bloom programs: handling channel messages via lookups in a table.
211
222
 
@@ -218,49 +229,51 @@ Given our understanding of the server, the client should be pretty simple. It n
218
229
 
219
230
  And here's the code:
220
231
 
221
- require 'rubygems'
222
- require 'bud'
223
- require 'chat_protocol'
224
-
225
- class ChatClient
226
- include Bud
227
- include ChatProtocol
228
-
229
- def initialize(nick, server, opts={})
230
- @nick = nick
231
- @server = server
232
- super opts
233
- end
234
-
235
- bootstrap do
236
- connect <~ [[@server, [ip_port, @nick]]]
237
- end
238
-
239
- bloom do
240
- mcast <~ stdio do |s|
241
- [@server, [ip_port, @nick, Time.new.strftime("%I:%M.%S"), s.line]]
242
- end
243
-
244
- stdio <~ mcast { |m| [pretty_print(m.val)] }
245
- end
246
-
247
- # format chat messages with timestamp on the right of the screen
248
- def pretty_print(val)
249
- str = val[1].to_s + ": " + (val[3].to_s || '')
250
- pad = "(" + val[2].to_s + ")"
251
- return str + " "*[66 - str.length,2].max + pad
252
- end
253
- end
232
+ ``` ruby
233
+ require 'rubygems'
234
+ require 'bud'
235
+ require_relative 'chat_protocol'
236
+
237
+ class ChatClient
238
+ include Bud
239
+ include ChatProtocol
240
+
241
+ def initialize(nick, server, opts={})
242
+ @nick = nick
243
+ @server = server
244
+ super opts
245
+ end
254
246
 
255
- if ARGV.length == 2
256
- server = ARGV[1]
257
- else
258
- server = ChatProtocol::DEFAULT_ADDR
247
+ bootstrap do
248
+ connect <~ [[@server, ip_port, @nick]]
249
+ end
250
+
251
+ bloom do
252
+ mcast <~ stdio do |s|
253
+ [@server, [ip_port, @nick, Time.new.strftime("%I:%M.%S"), s.line]]
259
254
  end
260
255
 
261
- puts "Server address: #{server}"
262
- program = ChatClient.new(ARGV[0], server, :read_stdin => true)
263
- program.run_fg
256
+ stdio <~ mcast { |m| [pretty_print(m.val)] }
257
+ end
258
+
259
+ # format chat messages with timestamp on the right of the screen
260
+ def pretty_print(val)
261
+ str = val[1].to_s + ": " + (val[3].to_s || '')
262
+ pad = "(" + val[2].to_s + ")"
263
+ return str + " "*[66 - str.length,2].max + pad
264
+ end
265
+ end
266
+
267
+ if ARGV.length == 2
268
+ server = ARGV[1]
269
+ else
270
+ server = ChatProtocol::DEFAULT_ADDR
271
+ end
272
+
273
+ puts "Server address: #{server}"
274
+ program = ChatClient.new(ARGV[0], server, :stdin => $stdin)
275
+ program.run_fg
276
+ ```
264
277
 
265
278
  The ChatClient class has a typical Ruby `initialize` method that sets up two local instance variables: one for this client's nickname, and another for the 'IP:port' address string for the server. It then calls the initializer of the Bud superclass passing along a hash of options.
266
279
 
@@ -293,4 +306,4 @@ In this section we saw a number of features that we missed in our earlier single
293
306
  * **the * operator and pairs method**: the way to combine items from multiple collections.
294
307
 
295
308
  # The Big Picture and the Details #
296
- Now that you've seen some working Bloom code, hopefully you're ready to delve deeper. The [README](README.md) provides links to places you can go for more information. Have fun and [stay in touch](http://groups.google.com/group/bloom-lang)!
309
+ Now that you've seen some working Bloom code, hopefully you're ready to delve deeper. The [README](README.md) provides links to places you can go for more information. Have fun and [stay in touch](http://groups.google.com/group/bloom-lang)!
@@ -58,7 +58,7 @@ Have a look at the following classic "transitive closure" example, which compute
58
58
 
59
59
  state do
60
60
  table :link, [:from, :to, :cost]
61
- table :path, [:from, :to, :cost]
61
+ table :path, [:from, :to, :cost]
62
62
  end
63
63
 
64
64
  bloom :make_paths do
@@ -67,7 +67,7 @@ Have a look at the following classic "transitive closure" example, which compute
67
67
 
68
68
  # recurse: path of length n+1 made by a link to a path of length n
69
69
  path <= (link*path).pairs(:to => :from) do |l,p|
70
- [l.from, p.to, l.cost+p.cost]
70
+ [l.from, p.to, l.cost + p.cost]
71
71
  end
72
72
  end
73
73
 
@@ -97,4 +97,4 @@ Note that it is possible to write a program in Bloom that is *unstratifiable*: t
97
97
 
98
98
  glass <= one_item {|t| ['full'] if glass.empty? }
99
99
 
100
- Consider the case where we start out with glass being empty. Then we know the fact `glass.empty?`, and the bloom statement says that `(glass.empty? => not glass.empty?)` which is equivalent to `(glass.empty? and not glass.empty?)` which is a contradiction. The Bud runtime detects cycles through non-monotonicity for you automatically when you instantiate your class.
100
+ Consider the case where we start out with glass being empty. Then we know the fact `glass.empty?`, and the bloom statement says that `(glass.empty? => not glass.empty?)` which is equivalent to `(glass.empty? and not glass.empty?)` which is a contradiction. The Bud runtime detects cycles through non-monotonicity for you automatically when you instantiate your class.
@@ -36,7 +36,7 @@ program = ShortestPaths.new
36
36
 
37
37
  # populate our little example. we put two links between 'a' and 'b'
38
38
  # to see whether our shortest-paths code does the right thing.
39
- program.link <= [['a', 'b', 1],
39
+ program.link <+ [['a', 'b', 1],
40
40
  ['a', 'b', 4],
41
41
  ['b', 'c', 1],
42
42
  ['c', 'd', 1],
@@ -48,6 +48,6 @@ program.shortest.to_a.sort.each {|t| puts t.inspect}
48
48
  puts "----"
49
49
 
50
50
  # now lets add an extra link and recompute
51
- program.link << ['e', 'f', 1]
51
+ program.link <+ [['e', 'f', 1]]
52
52
  program.tick
53
53
  program.shortest.to_a.sort.each {|t| puts t.inspect}
@@ -7,3 +7,5 @@ To run the chat example, do each of the following in a different terminal:
7
7
  # ruby chat.rb bob
8
8
 
9
9
  # ruby chat.rb harvey
10
+
11
+ Note that the "backports" gem should be installed.
@@ -1,6 +1,7 @@
1
1
  require 'rubygems'
2
+ require 'backports'
2
3
  require 'bud'
3
- require 'chat_protocol'
4
+ require_relative 'chat_protocol'
4
5
 
5
6
  class ChatClient
6
7
  include Bud
@@ -13,7 +14,7 @@ class ChatClient
13
14
  end
14
15
 
15
16
  bootstrap do
16
- connect <~ [[@server, [ip_port, @nick]]]
17
+ connect <~ [[@server, ip_port, @nick]]
17
18
  end
18
19
 
19
20
  bloom do
@@ -1,8 +1,8 @@
1
1
  module ChatProtocol
2
2
  state do
3
+ channel :connect, [:@addr, :client] => [:nick]
3
4
  channel :mcast
4
- channel :connect
5
5
  end
6
6
 
7
- DEFAULT_ADDR = "localhost:12345"
7
+ DEFAULT_ADDR = "127.0.0.1:12345"
8
8
  end
@@ -1,6 +1,7 @@
1
1
  require 'rubygems'
2
+ require 'backports'
2
3
  require 'bud'
3
- require 'chat_protocol'
4
+ require_relative 'chat_protocol'
4
5
 
5
6
  class ChatServer
6
7
  include Bud
@@ -9,7 +10,7 @@ class ChatServer
9
10
  state { table :nodelist }
10
11
 
11
12
  bloom do
12
- nodelist <= connect.payloads
13
+ nodelist <= connect { |c| [c.client, c.nick] }
13
14
  mcast <~ (mcast * nodelist).pairs { |m,n| [n.key, m.val] }
14
15
  end
15
16
  end
data/lib/bud.rb CHANGED
@@ -1,16 +1,27 @@
1
1
  require 'rubygems'
2
+ gem 'ruby2ruby', '>= 2.0.1'
3
+ gem 'ruby_parser', '>= 3.0.2'
4
+
2
5
  require 'eventmachine'
3
6
  require 'msgpack'
7
+ require 'ruby2ruby'
8
+ require 'ruby_parser'
9
+ require 'set'
4
10
  require 'socket'
5
11
  require 'superators19'
6
12
  require 'thread'
7
- require 'bud/errors'
8
13
 
14
+ require 'bud/errors'
9
15
  require 'bud/monkeypatch'
10
16
 
11
17
  require 'bud/aggs'
12
18
  require 'bud/bud_meta'
13
19
  require 'bud/collections'
20
+ require 'bud/executor/elements.rb'
21
+ require 'bud/executor/group.rb'
22
+ require 'bud/executor/join.rb'
23
+ require 'bud/lattice-core'
24
+ require 'bud/lattice-lib'
14
25
  require 'bud/metrics'
15
26
  require 'bud/rtrace'
16
27
  require 'bud/server'
@@ -19,10 +30,6 @@ require 'bud/storage/dbm'
19
30
  require 'bud/storage/zookeeper'
20
31
  require 'bud/viz'
21
32
 
22
- require 'bud/executor/elements.rb'
23
- require 'bud/executor/group.rb'
24
- require 'bud/executor/join.rb'
25
-
26
33
  ILLEGAL_INSTANCE_ID = -1
27
34
  SIGNAL_CHECK_PERIOD = 0.2
28
35
 
@@ -61,12 +68,13 @@ $bud_instances = {} # Map from instance id => Bud instance
61
68
  # :main: Bud
62
69
  module Bud
63
70
  attr_reader :budtime, :inbound, :options, :meta_parser, :viz, :rtracer, :dsock
64
- attr_reader :tables, :builtin_tables, :channels, :zk_tables, :dbm_tables, :app_tables
71
+ attr_reader :tables, :builtin_tables, :channels, :zk_tables, :dbm_tables, :app_tables, :lattices
65
72
  attr_reader :push_sources, :push_elems, :push_joins, :scanners, :merge_targets
66
- attr_reader :this_stratum, :this_rule, :rule_orig_src, :done_bootstrap
73
+ attr_reader :this_stratum, :this_rule_context, :done_bootstrap
74
+ attr_reader :inside_tick
67
75
  attr_accessor :stratified_rules
68
76
  attr_accessor :metrics, :periodics
69
- attr_accessor :this_rule_context, :qualified_name
77
+ attr_accessor :qualified_name
70
78
  attr_reader :running_async
71
79
 
72
80
  # options to the Bud runtime are passed in a hash, with the following keys
@@ -101,14 +109,12 @@ module Bud
101
109
  # * <tt>:dbm_dir</tt> filesystem directory to hold DBM-backed collections
102
110
  # * <tt>:dbm_truncate</tt> if true, DBM-backed collections are opened with +OTRUNC+
103
111
  def initialize(options={})
104
- # capture the binding for a subsequent 'eval'. This ensures that local
105
- # variable names introduced later in this method don't interfere with
106
- # table names used in the eval block.
107
112
  options[:dump_rewrite] ||= ENV["BUD_DUMP_REWRITE"].to_i > 0
108
113
  options[:dump_ast] ||= ENV["BUD_DUMP_AST"].to_i > 0
109
114
  options[:print_wiring] ||= ENV["BUD_PRINT_WIRING"].to_i > 0
110
115
  @qualified_name = ""
111
116
  @tables = {}
117
+ @lattices = {}
112
118
  @channels = {}
113
119
  @dbm_tables = {}
114
120
  @zk_tables = {}
@@ -130,7 +136,8 @@ module Bud
130
136
  @instance_id = ILLEGAL_INSTANCE_ID # Assigned when we start running
131
137
  @metrics = {}
132
138
  @endtime = nil
133
- @this_stratum = 0
139
+ @this_stratum = -1
140
+ @this_rule_id = -1
134
141
  @push_sorted_elems = nil
135
142
  @running_async = false
136
143
  @bud_started = false
@@ -144,23 +151,22 @@ module Bud
144
151
  # NB: If using an ephemeral port (specified by port = 0), the actual port
145
152
  # number won't be known until we start EM
146
153
 
154
+ load_lattice_defs
147
155
  builtin_state
148
156
  resolve_imports
149
157
  call_state_methods
150
158
 
151
- @declarations = self.class.instance_methods.select {|m| m =~ /^__bloom__.+$/}.map {|m| m.to_s}
152
-
153
159
  @viz = VizOnline.new(self) if @options[:trace]
154
160
  @rtracer = RTrace.new(self) if @options[:rtrace]
155
161
 
156
162
  do_rewrite
157
163
  if toplevel == self
158
164
  # initialize per-stratum state
159
- num_strata = @stratified_rules.length
160
- @scanners = num_strata.times.map{{}}
161
- @push_sources = num_strata.times.map{{}}
162
- @push_joins = num_strata.times.map{[]}
163
- @merge_targets = num_strata.times.map{Set.new}
165
+ @num_strata = @stratified_rules.length
166
+ @scanners = @num_strata.times.map{{}}
167
+ @push_sources = @num_strata.times.map{{}}
168
+ @push_joins = @num_strata.times.map{[]}
169
+ @merge_targets = @num_strata.times.map{Set.new}
164
170
  end
165
171
  end
166
172
 
@@ -248,16 +254,21 @@ module Bud
248
254
  tables[qname.to_sym] = t
249
255
  end
250
256
  end
257
+ mod_inst.lattices.each_pair do |name, t|
258
+ qname = "#{local_name}.#{name}".to_sym
259
+ raise Bud::Error if lattices.has_key? qname
260
+ lattices[qname] = t
261
+ end
251
262
  mod_inst.t_rules.each do |imp_rule|
252
263
  qname = "#{local_name}.#{imp_rule.lhs}"
253
264
  self.t_rules << [imp_rule.bud_obj, imp_rule.rule_id, qname, imp_rule.op,
254
- imp_rule.src, imp_rule.orig_src, imp_rule.nm_funcs_called]
265
+ imp_rule.src, imp_rule.orig_src, imp_rule.unsafe_funcs_called]
255
266
  end
256
267
  mod_inst.t_depends.each do |imp_dep|
257
268
  qlname = "#{local_name}.#{imp_dep.lhs}"
258
269
  qrname = "#{local_name}.#{imp_dep.body}"
259
270
  self.t_depends << [imp_dep.bud_obj, imp_dep.rule_id, qlname,
260
- imp_dep.op, qrname, imp_dep.nm]
271
+ imp_dep.op, qrname, imp_dep.nm, imp_dep.in_body]
261
272
  end
262
273
  mod_inst.t_provides.each do |imp_pro|
263
274
  qintname = "#{local_name}.#{imp_pro.interface}"
@@ -304,14 +315,13 @@ module Bud
304
315
  end
305
316
 
306
317
  def do_wiring
307
- @num_strata = @stratified_rules.length
308
-
309
318
  @stratified_rules.each_with_index { |rules, stratum| eval_rules(rules, stratum) }
310
319
 
311
320
  # Prepare list of tables that will be actively used at run time. First, all
312
- # the user-defined ones. We start @app_tables off as a set, then convert to
313
- # an array later.
321
+ # the user-defined tables and lattices. We start @app_tables off as a set,
322
+ # then convert to an array later.
314
323
  @app_tables = (@tables.keys - @builtin_tables.keys).map {|t| @tables[t]}.to_set
324
+ @app_tables.merge(@lattices.values)
315
325
 
316
326
  # Check scan and merge_targets to see if any builtin_tables need to be added as well.
317
327
  @scanners.each do |scs|
@@ -331,11 +341,11 @@ module Bud
331
341
  seen = Set.new(working)
332
342
  sorted_elems = [] # sorted elements in this stratum
333
343
  while not working.empty?
334
- sorted_elems += working
344
+ sorted_elems.concat(working)
335
345
  wired_to = []
336
346
  working.each do |e|
337
347
  e.wirings.each do |out|
338
- if (out.class <= PushElement and not seen.member?(out))
348
+ if ((out.class <= PushElement || out.class <= LatticePushElement) and not seen.member?(out))
339
349
  seen << out
340
350
  wired_to << out
341
351
  end
@@ -352,6 +362,24 @@ module Bud
352
362
  end
353
363
  end
354
364
 
365
+ # We create "orphan" scanners for collections that don't appear on the RHS
366
+ # of any rules, but do appear on the LHS of at least one rule. These
367
+ # scanners aren't needed to compute the fixpoint, but they are used as part
368
+ # of rescan/invalidation (e.g., if an orphaned collection receives a manual
369
+ # deletion operation, we need to arrange for the collection to be
370
+ # re-filled).
371
+ @orphan_scanners = [] # Pairs of [scanner, stratum]
372
+ @app_tables.each do |t|
373
+ next unless t.class <= Bud::BudCollection # skip lattice wrappers
374
+ next if t.scanner_cnt > 0
375
+
376
+ stratum = collection_stratum(t.qualified_tabname.to_s)
377
+ # if the collection also doesn't appear on any LHSs, skip it
378
+ next if stratum.nil?
379
+ @orphan_scanners << [Bud::ScannerElement.new(t.tabname, self, t, t.schema),
380
+ stratum]
381
+ end
382
+
355
383
  # Sanity check
356
384
  @push_sorted_elems.each do |stratum_elems|
357
385
  stratum_elems.each {|se| se.check_wiring}
@@ -417,19 +445,17 @@ module Bud
417
445
  #
418
446
  # scanner[stratum].rescan_set = Similar to above.
419
447
  def prepare_invalidation_scheme
420
- num_strata = @push_sorted_elems.size
421
448
  if $BUD_SAFE
422
- @app_tables = @tables.values # No tables excluded
449
+ @app_tables = @tables.values + @lattices.values # No collections excluded
423
450
 
424
451
  rescan = Set.new
425
452
  invalidate = @app_tables.select {|t| t.class <= BudScratch}.to_set
426
- num_strata.times do |stratum|
453
+ @num_strata.times do |stratum|
427
454
  @push_sorted_elems[stratum].each do |elem|
428
455
  invalidate << elem
429
456
  rescan << elem
430
457
  end
431
458
  end
432
- #prune_rescan_invalidate(rescan, invalidate)
433
459
  @default_rescan = rescan.to_a
434
460
  @default_invalidate = invalidate.to_a
435
461
  @reset_list = [] # Nothing to reset at end of tick. It'll be overwritten anyway
@@ -438,15 +464,19 @@ module Bud
438
464
 
439
465
  # By default, all tables are considered sources unless they appear on the
440
466
  # lhs. We only consider non-temporal rules because invalidation is only
441
- # about this tick. Also, we track (in nm_targets) those tables that are the
442
- # targets of user-defined code blocks that call non-monotonic functions
443
- # (such as budtime). Elements that feed these tables are forced to rescan
444
- # their contents, and thus forced to re-execute these code blocks.
445
- nm_targets = Set.new
467
+ # about this tick. Also, we track (in unsafe_targets) those tables that are
468
+ # the targets of user-defined code blocks that call "unsafe" functions that
469
+ # produce a different value in every tick (e.g., budtime). Elements that
470
+ # feed these tables are forced to rescan their contents, and thus forced to
471
+ # re-execute these code blocks.
472
+ unsafe_targets = Set.new
446
473
  t_rules.each do |rule|
447
474
  lhs = rule.lhs.to_sym
448
- @tables[lhs].is_source = false if rule.op == "<="
449
- nm_targets << lhs if rule.nm_funcs_called
475
+ if rule.op == "<="
476
+ # Note that lattices cannot be sources
477
+ @tables[lhs].is_source = false if @tables.has_key? lhs
478
+ end
479
+ unsafe_targets << lhs if rule.unsafe_funcs_called
450
480
  end
451
481
 
452
482
  # Compute a set of tables and elements that should be explicitly told to
@@ -455,54 +485,102 @@ module Bud
455
485
  invalidate = @app_tables.select {|t| t.invalidate_at_tick}.to_set
456
486
  rescan = Set.new
457
487
 
458
- num_strata.times do |stratum|
488
+ @num_strata.times do |stratum|
459
489
  @push_sorted_elems[stratum].each do |elem|
460
490
  rescan << elem if elem.rescan_at_tick
461
491
 
462
- if elem.outputs.any?{|tab| not(tab.class <= PushElement) and nm_targets.member? tab.qualified_tabname.to_sym }
492
+ if elem.outputs.any?{|tab| not(tab.class <= PushElement) and not(tab.class <= LatticePushElement) and unsafe_targets.member? tab.qualified_tabname.to_sym }
463
493
  rescan.merge(elem.wired_by)
464
494
  end
465
495
  end
466
496
  rescan_invalidate_tc(stratum, rescan, invalidate)
467
497
  end
468
498
 
469
- prune_rescan_invalidate(rescan, invalidate)
470
- # transitive closure
471
499
  @default_rescan = rescan.to_a
472
500
  @default_invalidate = invalidate.to_a
473
501
 
474
- puts "Default rescan: #{rescan.inspect}" if $BUD_DEBUG
475
- puts "Default inval: #{invalidate.inspect}" if $BUD_DEBUG
502
+ if $BUD_DEBUG
503
+ puts "Default rescan: #{rescan.inspect}"
504
+ puts "Default inval: #{invalidate.inspect}"
505
+ puts "Unsafe targets: #{unsafe_targets.inspect}"
506
+ end
476
507
 
477
- # Now compute for each table that is to be scanned, the set of dependent
478
- # tables and elements that will be invalidated if that table were to be
479
- # invalidated at run time.
508
+ # For each collection that is to be scanned, compute the set of dependent
509
+ # tables and elements that will need invalidation and/or rescan if that
510
+ # table were to be invalidated at runtime.
480
511
  dflt_rescan = rescan
481
512
  dflt_invalidate = invalidate
482
513
  to_reset = rescan + invalidate
483
- num_strata.times do |stratum|
484
- @scanners[stratum].each_value do |scanner|
485
- # If it is going to be always invalidated, it doesn't need further
486
- # examination
487
- next if dflt_rescan.member? scanner
488
-
489
- rescan = dflt_rescan + [scanner] # add scanner to scan set
490
- invalidate = dflt_invalidate.clone
491
- rescan_invalidate_tc(stratum, rescan, invalidate)
492
- prune_rescan_invalidate(rescan, invalidate)
493
- to_reset.merge(rescan)
494
- to_reset.merge(invalidate)
495
- # Give the diffs (from default) to scanner; these are elements that are
496
- # dependent on this scanner
497
- diffscan = (rescan - dflt_rescan).find_all {|elem| elem.class <= PushElement}
498
- scanner.invalidate_at_tick(diffscan, (invalidate - dflt_invalidate).to_a)
499
- end
514
+ each_scanner do |scanner, stratum|
515
+ # If it is going to be always invalidated, it doesn't need further
516
+ # examination. Lattice scanners also don't get invalidated.
517
+ next if dflt_rescan.member? scanner
518
+ next if scanner.class <= LatticeScanner
519
+
520
+ rescan = dflt_rescan.clone
521
+ invalidate = dflt_invalidate + [scanner.collection]
522
+ rescan_invalidate_tc(stratum, rescan, invalidate)
523
+ prune_rescan_set(rescan)
524
+
525
+ # Make sure we reset the rescan/invalidate flag for this scanner at
526
+ # end-of-tick, but we can remove the scanner from its own
527
+ # rescan_set/inval_set.
528
+ to_reset.merge(rescan)
529
+ to_reset.merge(invalidate)
530
+ rescan.delete(scanner)
531
+ invalidate.delete(scanner.collection)
532
+
533
+ # Give the diffs (from default) to scanner; these are elements that are
534
+ # dependent on this scanner
535
+ diffscan = (rescan - dflt_rescan).find_all {|elem| elem.class <= PushElement}
536
+ scanner.invalidate_at_tick(diffscan, (invalidate - dflt_invalidate).to_a)
500
537
  end
501
538
  @reset_list = to_reset.to_a
539
+
540
+ # For each lattice, find the collections that should be rescanned when there
541
+ # is a new delta for the lattice. That is, if we have a rule like:
542
+ # "t2 <= t1 {|t| [t.key, lat_foo]}", whenever there is a delta on lat_foo we
543
+ # should rescan t1 (to produce tuples with the updated lat_foo value).
544
+ #
545
+ # TODO:
546
+ # (1) if t1 is fed by rules r1 and r2 but only r1 references lattice x,
547
+ # don't trigger rescan of r2 on deltas for x (hard)
548
+ t_depends.each do |dep|
549
+ src, target_name = dep.body.to_sym, dep.lhs.to_sym
550
+ if @lattices.has_key? src and dep.in_body
551
+ src_lat = @lattices[src]
552
+ if @tables.has_key? target_name
553
+ target = @tables[target_name]
554
+ else
555
+ target = @lattices[target_name]
556
+ end
557
+
558
+ # Conservatively, we rescan all the elements that feed the lhs (target)
559
+ # collection via positive (non-deletion) rules; we then also need to
560
+ # potentially rescan ancestors of those elements as well (e.g., setting
561
+ # a stateless PushElement to rescan does nothing; we want to tell its
562
+ # ancestor ScannerElement to rescan).
563
+ #
564
+ # XXX: do we need to consider all transitively reachable nodes for
565
+ # rescan?
566
+ lat_rescan = target.positive_predecessors.to_set
567
+ lat_inval = Set.new
568
+ target.positive_predecessors.each do |e|
569
+ e.add_rescan_invalidate(lat_rescan, lat_inval)
570
+ end
571
+ src_lat.rescan_on_delta.merge(lat_rescan)
572
+ end
573
+ end
502
574
  end
503
575
 
504
- # given rescan, invalidate sets, compute transitive closure
576
+ # Given rescan, invalidate sets, compute transitive closure
505
577
  def rescan_invalidate_tc(stratum, rescan, invalidate)
578
+ # XXX: hack. If there's nothing in the given stratum, don't do
579
+ # anything. This can arise if we have an orphan scanner whose input is a
580
+ # non-monotonic operator; the stratum(LHS) = stratum(RHS) + 1, but there's
581
+ # nothing else in stratum(LHS).
582
+ return if @push_sorted_elems[stratum].nil?
583
+
506
584
  rescan_len = rescan.size
507
585
  invalidate_len = invalidate.size
508
586
  while true
@@ -515,12 +593,24 @@ module Bud
515
593
  end
516
594
  end
517
595
 
518
- def prune_rescan_invalidate(rescan, invalidate)
596
+ def prune_rescan_set(rescan)
519
597
  rescan.delete_if {|e| e.rescan_at_tick}
520
598
  end
521
599
 
600
+ def each_scanner
601
+ @num_strata.times do |stratum|
602
+ @scanners[stratum].each_value do |scanner|
603
+ yield scanner, stratum
604
+ end
605
+ end
606
+
607
+ @orphan_scanners.each do |scanner,stratum|
608
+ yield scanner, stratum
609
+ end
610
+ end
611
+
522
612
  def do_rewrite
523
- @meta_parser = BudMeta.new(self, @declarations)
613
+ @meta_parser = BudMeta.new(self)
524
614
  @stratified_rules = @meta_parser.meta_rewrite
525
615
  end
526
616
 
@@ -641,16 +731,30 @@ module Bud
641
731
  # method blocks until Bud has been shutdown. If +stop_em+ is true, the
642
732
  # EventMachine event loop is also shutdown; this will interfere with the
643
733
  # execution of any other Bud instances in the same process (as well as
644
- # anything else that happens to use EventMachine).
734
+ # anything else that happens to use EventMachine). We always shutdown the EM
735
+ # loop if there are no more running Bud instances (this does interfere with
736
+ # other EM-using apps, but it is necessary).
645
737
  def stop(stop_em=false, do_shutdown_cb=true)
646
738
  schedule_and_wait do
647
739
  do_shutdown(do_shutdown_cb)
648
740
  end
649
741
 
742
+ # If we're shutting down the last active Bud instance, shutdown the EM event
743
+ # loop as well. This is probably good practice in general, but it also
744
+ # prevents weird EM behavior -- it seems as though EM::ConnectionNotBound
745
+ # exceptions can be raised if the EM event loop is left running and
746
+ # subsequent events arrive.
747
+ $signal_lock.synchronize {
748
+ stop_em = true if $bud_instances.empty? and EventMachine::reactor_running?
749
+ }
750
+
650
751
  if stop_em
651
752
  Bud.stop_em_loop
652
- EventMachine::reactor_thread.join
753
+ unless Thread.current == EventMachine::reactor_thread
754
+ EventMachine::reactor_thread.join
755
+ end
653
756
  end
757
+
654
758
  report_metrics if options[:metrics]
655
759
  end
656
760
  alias :stop_bg :stop
@@ -962,10 +1066,42 @@ module Bud
962
1066
  end
963
1067
  bootstrap
964
1068
 
965
- @tables.each_value {|t| t.bootstrap} if toplevel == self
1069
+ if toplevel == self
1070
+ @tables.each_value {|t| t.bootstrap}
1071
+ @lattices.each_value {|l| l.bootstrap}
1072
+ end
966
1073
  @done_bootstrap = true
967
1074
  end
968
1075
 
1076
+ def do_invalidate_rescan
1077
+ @default_rescan.each {|elem| elem.rescan = true}
1078
+ @default_invalidate.each {|elem|
1079
+ elem.invalidated = true
1080
+ # Call tick on tables here itself. The rest below
1081
+ elem.invalidate_cache unless elem.class <= PushElement
1082
+ }
1083
+
1084
+ # The following loop invalidates additional (non-default) elements and
1085
+ # tables that depend on the run-time invalidation state of a table. Loop
1086
+ # once to set the flags.
1087
+ each_scanner do |scanner, stratum|
1088
+ if scanner.rescan
1089
+ scanner.rescan_set.each {|e| e.rescan = true}
1090
+ scanner.invalidate_set.each {|e|
1091
+ e.invalidated = true
1092
+ e.invalidate_cache unless e.class <= PushElement
1093
+ }
1094
+ end
1095
+ end
1096
+
1097
+ # Loop a second time to actually call invalidate_cache. We can't merge this
1098
+ # with the loops above because some versions of invalidate_cache (e.g.,
1099
+ # join) depend on the rescan state of other elements.
1100
+ @num_strata.times do |stratum|
1101
+ @push_sorted_elems[stratum].each {|e| e.invalidate_cache if e.invalidated}
1102
+ end
1103
+ end
1104
+
969
1105
  # One timestep of Bloom execution. This MUST be invoked from the EventMachine
970
1106
  # thread; it is not intended to be called directly by client code.
971
1107
  def tick_internal
@@ -985,32 +1121,7 @@ module Bud
985
1121
  else
986
1122
  # inform tables and elements about beginning of tick.
987
1123
  @app_tables.each {|t| t.tick}
988
- @default_rescan.each {|elem| elem.rescan = true}
989
- @default_invalidate.each {|elem|
990
- elem.invalidated = true
991
- # Call tick on tables here itself. The rest below
992
- elem.invalidate_cache unless elem.class <= PushElement
993
- }
994
-
995
- num_strata = @push_sorted_elems.size
996
- # The following loop invalidates additional (non-default) elements and
997
- # tables that depend on the run-time invalidation state of a table.
998
- # Loop once to set the flags.
999
- num_strata.times do |stratum|
1000
- @scanners[stratum].each_value do |scanner|
1001
- if scanner.rescan
1002
- scanner.rescan_set.each {|e| e.rescan = true}
1003
- scanner.invalidate_set.each {|e|
1004
- e.invalidated = true
1005
- e.invalidate_cache unless e.class <= PushElement
1006
- }
1007
- end
1008
- end
1009
- end
1010
- # Loop a second time to actually call invalidate_cache
1011
- num_strata.times do |stratum|
1012
- @push_sorted_elems[stratum].each {|e| e.invalidate_cache if e.invalidated}
1013
- end
1124
+ do_invalidate_rescan
1014
1125
  end
1015
1126
 
1016
1127
  receive_inbound
@@ -1074,14 +1185,14 @@ module Bud
1074
1185
  end
1075
1186
 
1076
1187
  # Return the stratum number of the given collection.
1077
- # NB: if a collection is not referenced by any rules, it is not currently
1078
- # assigned to a strata.
1188
+ # NB: if a collection does not appear on the lhs or rhs of any rules, it is
1189
+ # not currently assigned to a strata.
1079
1190
  def collection_stratum(collection)
1080
1191
  t_stratum.each do |t|
1081
1192
  return t.stratum if t.predicate == collection
1082
1193
  end
1083
1194
 
1084
- raise Bud::Error, "no such collection: #{collection}"
1195
+ return nil
1085
1196
  end
1086
1197
 
1087
1198
  private
@@ -1099,25 +1210,26 @@ module Bud
1099
1210
  @periodics = table :periodics_tbl, [:pername] => [:period]
1100
1211
 
1101
1212
  # for BUD reflection
1102
- table :t_rules, [:bud_obj, :rule_id] => [:lhs, :op, :src, :orig_src, :nm_funcs_called]
1103
- table :t_depends, [:bud_obj, :rule_id, :lhs, :op, :body] => [:nm]
1213
+ table :t_cycle, [:predicate, :via, :neg, :temporal]
1214
+ table :t_depends, [:bud_obj, :rule_id, :lhs, :op, :body] => [:nm, :in_body]
1104
1215
  table :t_provides, [:interface] => [:input]
1105
- table :t_underspecified, t_provides.schema
1216
+ table :t_rule_stratum, [:bud_obj, :rule_id] => [:stratum]
1217
+ table :t_rules, [:bud_obj, :rule_id] => [:lhs, :op, :src, :orig_src, :unsafe_funcs_called]
1106
1218
  table :t_stratum, [:predicate] => [:stratum]
1107
- table :t_cycle, [:predicate, :via, :neg, :temporal]
1108
1219
  table :t_table_info, [:tab_name, :tab_type]
1109
1220
  table :t_table_schema, [:tab_name, :col_name, :ord, :loc]
1221
+ table :t_underspecified, t_provides.schema
1110
1222
 
1111
1223
  # Identify builtin tables as such
1112
1224
  @builtin_tables = @tables.clone if toplevel
1113
1225
  end
1114
1226
 
1115
- # Handle any inbound tuples off the wire. Received messages are placed
1116
- # directly into the storage of the appropriate local channel. The inbound
1117
- # queue is cleared at the end of the tick.
1227
+ # Handle external inputs: channels, terminals, and periodics. Received
1228
+ # messages are placed directly into the storage of the appropriate local
1229
+ # collection. The inbound queue is cleared at the end of the tick.
1118
1230
  def receive_inbound
1119
1231
  @inbound.each do |tbl_name, msg_buf|
1120
- puts "channel #{tbl_name} rcv: #{msg_buf}" if $BUD_DEBUG
1232
+ puts "recv via #{tbl_name}: #{msg_buf}" if $BUD_DEBUG
1121
1233
  msg_buf.each do |b|
1122
1234
  tables[tbl_name] << b
1123
1235
  end
@@ -1142,17 +1254,20 @@ module Bud
1142
1254
  # of PushElements
1143
1255
  @this_stratum = strat_num
1144
1256
  rules.each_with_index do |rule, i|
1145
- @this_rule_context = rule.bud_obj # user-supplied code blocks will be evaluated in this context at run-time
1257
+ # user-supplied code blocks will be evaluated in this context at run-time
1258
+ @this_rule_context = rule.bud_obj
1146
1259
  begin
1147
1260
  eval_rule(rule.bud_obj, rule.src)
1148
1261
  rescue Exception => e
1149
- err_msg = "** Exception while wiring rule: #{rule.src}\n ****** #{e}"
1262
+ err_msg = "** Exception while wiring rule: #{rule.orig_src}\n ****** #{e}"
1150
1263
  # Create a new exception for accomodating err_msg, but reuse original backtrace
1151
1264
  new_e = (e.class <= Bud::Error) ? e.class.new(err_msg) : Bud::Error.new(err_msg)
1152
1265
  new_e.set_backtrace(e.backtrace)
1153
1266
  raise new_e
1154
1267
  end
1155
1268
  end
1269
+ @this_rule_context = nil
1270
+ @this_stratum = -1
1156
1271
  end
1157
1272
 
1158
1273
  ######## ids and timers
@@ -1183,10 +1298,10 @@ module Bud
1183
1298
  EventMachine::release_machine
1184
1299
  EventMachine::instance_variable_set('@reactor_running', false)
1185
1300
  end
1301
+
1186
1302
  # Shutdown all the Bud instances inherited from the parent process, but
1187
1303
  # don't invoke their shutdown callbacks
1188
1304
  Bud.shutdown_all_instances(false)
1189
-
1190
1305
  $got_shutdown_signal = false
1191
1306
  $signal_handler_setup = false
1192
1307
 
@@ -1206,16 +1321,16 @@ module Bud
1206
1321
  end
1207
1322
 
1208
1323
  # Signal handling. If multiple Bud instances are running inside a single
1209
- # process, we want a SIGINT or SIGTERM signal to cleanly shutdown all of them.
1324
+ # process, we want a SIGINT or SIGTERM signal to cleanly shutdown all of
1325
+ # them. Note that we don't try to do any significant work in the signal
1326
+ # handlers themselves: we just set a flag that is checked by a periodic timer.
1210
1327
  def self.init_signal_handlers(b)
1211
1328
  $signal_lock.synchronize {
1212
- # If we setup signal handlers and then fork a new process, we want to
1213
- # reinitialize the signal handler in the child process.
1329
+ # Initialize or re-initialize signal handlers if necessary.
1214
1330
  unless b.options[:signal_handling] == :none || $signal_handler_setup
1215
1331
  EventMachine::PeriodicTimer.new(SIGNAL_CHECK_PERIOD) do
1216
1332
  if $got_shutdown_signal
1217
1333
  Bud.shutdown_all_instances
1218
- Bud.stop_em_loop
1219
1334
  $got_shutdown_signal = false
1220
1335
  end
1221
1336
  end