bluth 0.6.8 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES.txt CHANGED
@@ -1,5 +1,14 @@
1
1
  BLUTH, CHANGES
2
2
 
3
+ #### 0.7.0 (2011-03-04) ###############################
4
+
5
+ * ADDED: backtrace field to Bluth::Gob
6
+ * ADDED: Bluth::TimingBelt
7
+ * ADDED: Bluth::Queue.queues/entry_queues include TimingBelt queues
8
+ when Bluth::TimingBelt is defined.
9
+ * ADDED: replace-worker command
10
+
11
+
3
12
  #### 0.6.8 (2011-02-07) ###############################
4
13
 
5
14
  * FIXED: Remove use of calls to fineround in Worker
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :MAJOR: 0
3
- :MINOR: 6
4
- :PATCH: 8
3
+ :MINOR: 7
4
+ :PATCH: 0
data/bin/bluth CHANGED
@@ -68,14 +68,17 @@ class Bluth::CLI::Definition
68
68
 
69
69
  command :start_worker => Bluth::CLI
70
70
  command :start_scheduler => Bluth::CLI
71
-
71
+
72
72
  option :f, :force
73
73
  command :stop_workers => Bluth::CLI
74
74
  option :f, :force
75
75
  command :stop_worker => Bluth::CLI
76
76
  option :f, :force
77
77
  command :stop_scheduler => Bluth::CLI
78
-
78
+
79
+ about "Stop the oldest worker and start a new instance in its place."
80
+ command :replace_worker => Bluth::CLI
81
+
79
82
  command :workers => Bluth::CLI
80
83
  command :schedulers => Bluth::CLI
81
84
 
data/bluth.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{bluth}
8
- s.version = "0.6.8"
8
+ s.version = "0.7.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Delano Mandelbaum"]
12
- s.date = %q{2011-02-08}
12
+ s.date = %q{2011-03-04}
13
13
  s.default_executable = %q{bluth}
14
14
  s.description = %q{A Redis queuing system built on top of Familia}
15
15
  s.email = %q{delano@solutious.com}
@@ -29,23 +29,24 @@ Gem::Specification.new do |s|
29
29
  "lib/bluth.rb",
30
30
  "lib/bluth/cli.rb",
31
31
  "lib/bluth/test_helpers.rb",
32
+ "lib/bluth/timingbelt.rb",
32
33
  "lib/bluth/worker.rb",
33
34
  "lib/daemonizing.rb",
34
35
  "try/15_queue_try.rb",
35
36
  "try/16_worker_try.rb",
36
37
  "try/17_gob_try.rb",
37
38
  "try/18_handler_try.rb",
38
- "try/19_bluth_try.rb"
39
+ "try/19_bluth_try.rb",
40
+ "try/30_timingbelt_try.rb"
39
41
  ]
40
42
  s.homepage = %q{http://github.com/delano/bluth}
41
43
  s.rdoc_options = ["--charset=UTF-8"]
42
44
  s.require_paths = ["lib"]
43
45
  s.rubyforge_project = %q{bluth}
44
- s.rubygems_version = %q{1.3.7}
46
+ s.rubygems_version = %q{1.5.2}
45
47
  s.summary = %q{A Redis queuing system built on top of Familia (w/ daemons!)}
46
48
 
47
49
  if s.respond_to? :specification_version then
48
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
49
50
  s.specification_version = 3
50
51
 
51
52
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
data/lib/bluth.rb CHANGED
@@ -88,7 +88,7 @@ module Bluth
88
88
 
89
89
  require 'bluth/worker'
90
90
 
91
- module Queue # if this is a module the
91
+ module Queue
92
92
  include Familia
93
93
  prefix [:bluth, :queue]
94
94
  class_list :critical #, :class => Bluth::Gob
@@ -98,16 +98,41 @@ module Bluth
98
98
  class_list :successful
99
99
  class_list :failed
100
100
  class_list :orphaned
101
+ def self.create_queue name
102
+ unless queue(name)
103
+ q = Familia::List.new name, :parent => self
104
+ @queuecache[name.to_s.to_sym] = q
105
+ end
106
+ queue(name)
107
+ end
101
108
  class << self
102
109
  # The complete list of queues in the order they were defined
103
110
  def queues
104
- Bluth::Queue.class_lists.collect(&:name).collect do |qname|
111
+ qs = Bluth::Queue.class_lists.collect(&:name).collect do |qname|
105
112
  self.send qname
106
113
  end
114
+ if defined?(Bluth::TimingBelt)
115
+ notch_queues = Bluth::TimingBelt.priority.collect { |notch| notch.queue }
116
+ qs.insert 1, *notch_queues
117
+ end
118
+ qs
107
119
  end
108
120
  # The subset of queues that new jobs arrive in, in order of priority
109
121
  def entry_queues
110
- Bluth.priority.collect { |qname| self.send qname }
122
+ qs = Bluth.priority.collect { |qname| self.send qname }
123
+ if defined?(Bluth::TimingBelt)
124
+ notch_queues = Bluth::TimingBelt.priority.collect { |notch| notch.queue }
125
+ qs.insert 1, *notch_queues
126
+ end
127
+ qs
128
+ end
129
+ def queue name
130
+ if class_list? name.to_s.to_sym
131
+ self.send(name)
132
+ else
133
+ @queuecache ||= {}
134
+ @queuecache[name.to_s.to_sym]
135
+ end
111
136
  end
112
137
  end
113
138
 
@@ -115,9 +140,9 @@ module Bluth
115
140
  Bluth.priority = [:critical, :high, :low]
116
141
  end
117
142
 
118
- # Workers use a blocking pop and will wait for up to
119
- # Bluth.queuetimeout (seconds) before returnning nil.
120
- # Note that the queues are still processed in order.
143
+ # Workers use a blocking pop and will wait for up to
144
+ # Bluth.queuetimeout (seconds) before returnning nil.
145
+ # Note that the queues are still processed in order.
121
146
  # If all queues are empty, the first one to return a
122
147
  # value is use. See:
123
148
  #
@@ -137,6 +162,7 @@ module Bluth
137
162
  gob = nil
138
163
  begin
139
164
  order = Bluth::Queue.entry_queues.collect(&:rediskey)
165
+ Familia.ld " QUEUE ORDER: #{order.join(', ')}"
140
166
  order << Bluth.queuetimeout # We do it this way to support Ruby 1.8
141
167
  queue, gobid = *(Bluth::Queue.redis.send(meth, *order) || [])
142
168
  unless queue.nil?
@@ -179,12 +205,27 @@ module Bluth
179
205
  end
180
206
  end
181
207
 
182
- def enqueue(data={},q=nil)
183
- q = self.queue(q)
208
+ def engauge(data={}, notch=nil)
209
+ notch ||= Bluth::TimingBelt.notch 1
210
+ gob = create_job data
211
+ gob.notch = notch.name
212
+ gob.save
213
+ Familia.ld "ENNOTCHING: #{self} #{gob.jobid.short} to #{notch.rediskey}" if Familia.debug?
214
+ notch.add gob.jobid
215
+ gob
216
+ end
217
+
218
+ def create_job data={}
184
219
  gob = Gob.create generate_id(data), self, data
185
- gob.current_queue = q.name
186
220
  gob.created
187
221
  gob.attempts = 0
222
+ gob
223
+ end
224
+
225
+ def enqueue(data={}, q=nil)
226
+ q = self.queue(q) if q.nil? || Symbol === q
227
+ gob = create_job data
228
+ gob.current_queue = q.name
188
229
  gob.save
189
230
  Familia.ld "ENQUEUING: #{self} #{gob.jobid.short} to #{q}" if Familia.debug?
190
231
  q << gob.jobid
@@ -192,7 +233,7 @@ module Bluth
192
233
  end
193
234
  def queue(name=nil)
194
235
  @queue = name if name
195
- Bluth::Queue.send(@queue || :high)
236
+ Bluth::Queue.queue(@queue || :high)
196
237
  end
197
238
  def generate_id(*args)
198
239
  [self, Process.pid, Bluth.sysinfo.hostname, Time.now.to_f, *args].gibbler
@@ -222,6 +263,8 @@ module Bluth
222
263
  field :messages => Array
223
264
  field :attempts => Integer
224
265
  field :create_time => Float
266
+ field :backtrace
267
+ field :notch # populated only via TimingBelt
225
268
  field :stime => Float
226
269
  field :etime => Float
227
270
  field :current_queue => Symbol
data/lib/bluth/cli.rb CHANGED
@@ -45,17 +45,25 @@ module Bluth
45
45
  end
46
46
  end
47
47
 
48
- def stop_worker wid=nil,worker_class=Bluth::Worker
49
- wids = wid ? [wid] : @argv
48
+ def stop_worker wid=nil, worker_class=Bluth::Worker
50
49
  Bluth.connect
50
+ wids = wid ? [wid] : @argv
51
51
  wids.each do |wid|
52
52
  worker = worker_class.from_redis wid
53
53
  kill_worker worker, worker_class
54
54
  end
55
55
  end
56
56
 
57
+ def replace_worker worker_class=Bluth::Worker
58
+ Bluth.connect
59
+ @global.daemon = true
60
+ worker = worker_class.instances.first # grabs the oldest worker
61
+ kill_worker worker, worker_class
62
+ start_worker worker_class
63
+ end
64
+
57
65
  def workers worker_class=Bluth::Worker
58
- Familia.info worker_class.all.collect &:key
66
+ Familia.info worker_class.all.collect &:rediskey
59
67
  end
60
68
 
61
69
  private
@@ -0,0 +1,118 @@
1
+ require 'time'
2
+
3
+ module Bluth
4
+
5
+ module TimingBelt
6
+ include Familia
7
+ prefix [:bluth, :timingbelt]
8
+ # This module extends the Familia::Set that represents
9
+ # a notch. IOW, these are instance methods for notch objs.
10
+ module Notch
11
+ attr_accessor :stamp, :filter, :time
12
+ def next
13
+ skip
14
+ end
15
+ def prev
16
+ skip -1
17
+ end
18
+ def skip mins=1
19
+ time = Time.parse(stamp || '')
20
+ Bluth::TimingBelt.notch mins, filter, time
21
+ end
22
+ def queue
23
+ Bluth::Queue.create_queue stamp
24
+ end
25
+ def -(other)
26
+ ((self.time - other.time)/60).to_i
27
+ end
28
+ end
29
+ @length = 60 # minutes
30
+ class << self
31
+ attr_reader :notchcache, :length
32
+ def find v, mins=length, filter=nil, time=now
33
+ raise ArgumentError, "value cannot be nil" if v.nil?
34
+ select(mins, filter, time) do |notch|
35
+ notch.member?(v)
36
+ end
37
+ end
38
+ def range rng, filter=nil, time=now, &blk
39
+ rng.to_a.each { |idx|
40
+ notch = Bluth::TimingBelt.notch idx, filter, time
41
+ blk.call notch
42
+ }
43
+ end
44
+ # mins: the number of minutes to look ahead.
45
+ def each mins=length, filter=nil, time=now, &blk
46
+ mins.times { |idx|
47
+ notch = Bluth::TimingBelt.notch idx, filter, time
48
+ blk.call notch
49
+ }
50
+ end
51
+ def select mins=length, filter=nil, time=now, &blk
52
+ ret = []
53
+ each(mins, filter, time) { |notch| ret << notch if blk.call(notch) }
54
+ ret
55
+ end
56
+ def collect mins=length, filter=nil, time=now, &blk
57
+ ret = []
58
+ each(mins, filter, time) { |notch| ret << blk.call(notch) }
59
+ ret
60
+ end
61
+ def now mins=0, time=Time.now.utc
62
+ time + (mins*60) # time wants it in seconds
63
+ end
64
+ def stamp mins=0, time=now
65
+ (time + (mins*60)).strftime('%H:%M')
66
+ end
67
+ def notch mins=0, filter=nil, time=now
68
+ key = rediskey(stamp(mins, time), filter)
69
+ @notchcache ||= {}
70
+ if @notchcache[key].nil?
71
+ @notchcache[key] ||= Familia::Set.new key,
72
+ :ttl => 2*60*60, # 2 hours
73
+ :extend => Bluth::TimingBelt::Notch,
74
+ :db => Bluth::TimingBelt.db
75
+ @notchcache[key].stamp = stamp(mins, time)
76
+ @notchcache[key].filter = filter
77
+ @notchcache[key].time = now(mins, time)
78
+ end
79
+ @notchcache[key]
80
+ end
81
+ def priority minutes=2, filter=nil, time=now
82
+ (0..minutes).to_a.reverse.collect { |min| notch(min*-1, filter, time) }
83
+ end
84
+ def next_empty_notch filter=nil, time=now
85
+ length.times { |min|
86
+ possible = notch min+1, filter, time # add 1 so we don't start at 0
87
+ return possible if possible.empty?
88
+ }
89
+ nil
90
+ end
91
+ def add data, notch=nil
92
+ notch ||= Bluth::TimingBelt.notch 1
93
+ notch.add data
94
+ end
95
+ def pop minutes=2, filter=nil, time=now
96
+ gob = nil
97
+ priority = Bluth::TimingBelt.priority minutes, filter, time
98
+ begin
99
+ gobid, notch = nil, nil
100
+ priority.each { |n| gobid, notch = n.pop, n.name; break unless gobid.nil? }
101
+ unless gobid.nil?
102
+ Familia.ld "FOUND #{gobid} id #{notch}" if Familia.debug?
103
+ gob = Bluth::Gob.from_redis gobid
104
+ raise Bluth::Buster, "No such gob object: #{gobid}" if gob.nil?
105
+ Bluth::Queue.running << gob.jobid
106
+ gob.current_queue = :running
107
+ gob.save
108
+ end
109
+ rescue => ex
110
+ Familia.info ex.message
111
+ Familia.ld ex.backtrace if Familia.debug?
112
+ end
113
+ gob
114
+ end
115
+ end
116
+
117
+ end
118
+ end
data/lib/bluth/worker.rb CHANGED
@@ -200,7 +200,7 @@ module Bluth
200
200
 
201
201
  private
202
202
 
203
- # DO NOT return from this method
203
+ # DO NOT call return from this method
204
204
  def find_gob(task=nil)
205
205
  begin
206
206
  job = Bluth.pop
data/try/15_queue_try.rb CHANGED
@@ -27,4 +27,20 @@ Bluth::Queue.critical.size
27
27
  job = Bluth::Queue.critical.shift
28
28
  #=> 'job1'
29
29
 
30
+
31
+ ## Can create a queue on the fly
32
+ q = Bluth::Queue.create_queue :anything
33
+ q.rediskey
34
+ #=> "bluth:queue:anything"
35
+
36
+ ## And that new queue has a method
37
+ q = Bluth::Queue.queue :anything
38
+ q.class
39
+ #=> Familia::List
40
+
41
+ ## We can get a list of queues by priority
42
+ Bluth::Queue.entry_queues.collect { |q| q.name }
43
+ #=> [:critical, :high, :low]
44
+
45
+
30
46
  Bluth::Queue.critical.clear
data/try/17_gob_try.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'bluth'
2
2
  require 'bluth/test_helpers'
3
3
 
4
- Familia.debug = true
4
+ #Familia.debug = true
5
5
 
6
6
  ## Can enqueue a job
7
7
  @job = ExampleHandler.enqueue :arg1 => :val1
data/try/19_bluth_try.rb CHANGED
@@ -35,6 +35,7 @@ Bluth.queuetimeout = 2
35
35
  Bluth.pop
36
36
  #=> nil
37
37
 
38
+
38
39
  Bluth::Queue.critical.clear
39
40
  @job1.destroy! if @job1
40
41
  @job2.destroy! if @job2
@@ -0,0 +1,120 @@
1
+ require 'bluth'
2
+ require 'bluth/timingbelt'
3
+ require 'bluth/test_helpers'
4
+
5
+ #Familia.debug = true
6
+
7
+ @now = Time.at(1297641600).utc # 2011-02-14 20:00:00
8
+ Bluth::TimingBelt.redis.flushdb
9
+
10
+ ## Knows now
11
+ Bluth::TimingBelt.now(0, @now).to_s
12
+ #=> '2011-02-14 00:00:00 UTC'
13
+
14
+ ## Now can have an offset
15
+ Bluth::TimingBelt.now(5, @now).to_s
16
+ #=> '2011-02-14 00:05:00 UTC'
17
+
18
+ ## Can create a timestamp
19
+ Bluth::TimingBelt.stamp 0, @now
20
+ #=> '00:00'
21
+
22
+ ## Knows the current key
23
+ Bluth::TimingBelt.rediskey '00:00', nil
24
+ #=> 'bluth:timingbelt:00:00'
25
+
26
+ ## Creates a Set object for the current time
27
+ Bluth::TimingBelt.notch(0, nil, @now).class
28
+ #=> Familia::Set
29
+
30
+ ## A notch knows its stamp
31
+ Bluth::TimingBelt.notch(0, nil, @now).stamp
32
+ #=> '00:00'
33
+
34
+ ## A notch knows the next stamp
35
+ Bluth::TimingBelt.notch(0, nil, @now).next.stamp
36
+ #=> '00:01'
37
+
38
+ ## A notch knows the previous stamp
39
+ Bluth::TimingBelt.notch(0, nil, @now).prev.stamp
40
+ #=> '23:59'
41
+
42
+ ## A notch can skip to arbitrary number ahead
43
+ Bluth::TimingBelt.notch(0, nil, @now).skip(15).stamp
44
+ #=> '00:15'
45
+
46
+ ## Set for the current time doesn't exist
47
+ Bluth::TimingBelt.notch(0, nil, @now).exists?
48
+ #=> false
49
+
50
+ ## Set for the current time is empty
51
+ Bluth::TimingBelt.notch(0, nil, @now).empty?
52
+ #=> true
53
+
54
+ ## Knows the current set priority
55
+ Bluth::TimingBelt.priority(2, nil, @now).collect { |q| q.name }
56
+ #=> ["bluth:timingbelt:23:58", "bluth:timingbelt:23:59", "bluth:timingbelt:00:00"]
57
+
58
+ ## Handler can engauge right now
59
+ notch = Bluth::TimingBelt.notch(0, nil, @now)
60
+ ExampleHandler.engauge({}, notch).notch
61
+ #=> 'bluth:timingbelt:00:00'
62
+
63
+ ## Handler can engauge 1 minute ago
64
+ notch = Bluth::TimingBelt.notch(-1, nil, @now)
65
+ ExampleHandler.engauge({}, notch).notch
66
+ #=> 'bluth:timingbelt:23:59'
67
+
68
+ ## Handler can engauge 10 minutes from now
69
+ notch = Bluth::TimingBelt.notch(10, nil, @now)
70
+ @gob3 = ExampleHandler.engauge({}, notch)
71
+ @gob3.notch
72
+ #=> 'bluth:timingbelt:00:10'
73
+
74
+ ## Will get a job from the highest priority notch
75
+ @gob1 = Bluth::TimingBelt.pop(2, nil, @now)
76
+ @gob1.notch
77
+ #=> 'bluth:timingbelt:23:59'
78
+
79
+ ## Will get a job from the next priority notch
80
+ @gob2 = Bluth::TimingBelt.pop(2, nil, @now)
81
+ @gob2.notch
82
+ #=> 'bluth:timingbelt:00:00'
83
+
84
+ ## Knows next available notch
85
+ @next_notch = Bluth::TimingBelt.next_empty_notch(nil, @now)
86
+ @next_notch.name unless @next_notch.nil?
87
+ #=> 'bluth:timingbelt:00:01'
88
+
89
+ ## Knows next available notch
90
+ notches = Bluth::TimingBelt.find(@gob3.jobid, 60, nil, @now)
91
+ notches.first.name unless notches.first.nil?
92
+ #=> 'bluth:timingbelt:00:10'
93
+
94
+ ## Can calculate the difference between two notches
95
+ notch1 = Bluth::TimingBelt.notch
96
+ notch2 = Bluth::TimingBelt.notch 67
97
+ puts notch2.name
98
+ notch2 - notch1
99
+ #=> 67
100
+
101
+ ## A notch has an associated queue
102
+ notch = Bluth::TimingBelt.notch(0, nil, @now)
103
+ notch.queue.class
104
+ #=> Familia::List
105
+
106
+ ## And that queue has the same timestamp
107
+ notch = Bluth::TimingBelt.notch(0, nil, @now)
108
+ notch.queue.rediskey
109
+ #=> 'bluth:queue:00:00'
110
+
111
+ ## We can get a list of queues by priority
112
+ @current_notch = Bluth::TimingBelt.notch
113
+ Bluth::Queue.entry_queues.collect { |q| q.name }
114
+ #=> [:critical, @current_notch.prev.prev.queue.name, @current_notch.prev.queue.name, @current_notch.queue.name, :high, :low]
115
+
116
+ ## Just a test
117
+ Bluth.pop
118
+ ##=> true
119
+
120
+
metadata CHANGED
@@ -1,13 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bluth
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 6
9
- - 8
10
- version: 0.6.8
4
+ prerelease:
5
+ version: 0.7.0
11
6
  platform: ruby
12
7
  authors:
13
8
  - Delano Mandelbaum
@@ -15,7 +10,7 @@ autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
12
 
18
- date: 2011-02-08 00:00:00 -05:00
13
+ date: 2011-03-04 00:00:00 -05:00
19
14
  default_executable: bluth
20
15
  dependencies:
21
16
  - !ruby/object:Gem::Dependency
@@ -26,11 +21,6 @@ dependencies:
26
21
  requirements:
27
22
  - - ">="
28
23
  - !ruby/object:Gem::Version
29
- hash: 13
30
- segments:
31
- - 0
32
- - 6
33
- - 5
34
24
  version: 0.6.5
35
25
  type: :runtime
36
26
  version_requirements: *id001
@@ -42,11 +32,6 @@ dependencies:
42
32
  requirements:
43
33
  - - ">="
44
34
  - !ruby/object:Gem::Version
45
- hash: 5
46
- segments:
47
- - 0
48
- - 7
49
- - 3
50
35
  version: 0.7.3
51
36
  type: :runtime
52
37
  version_requirements: *id002
@@ -58,9 +43,6 @@ dependencies:
58
43
  requirements:
59
44
  - - ">="
60
45
  - !ruby/object:Gem::Version
61
- hash: 3
62
- segments:
63
- - 0
64
46
  version: "0"
65
47
  type: :runtime
66
48
  version_requirements: *id003
@@ -84,6 +66,7 @@ files:
84
66
  - lib/bluth.rb
85
67
  - lib/bluth/cli.rb
86
68
  - lib/bluth/test_helpers.rb
69
+ - lib/bluth/timingbelt.rb
87
70
  - lib/bluth/worker.rb
88
71
  - lib/daemonizing.rb
89
72
  - try/15_queue_try.rb
@@ -91,6 +74,7 @@ files:
91
74
  - try/17_gob_try.rb
92
75
  - try/18_handler_try.rb
93
76
  - try/19_bluth_try.rb
77
+ - try/30_timingbelt_try.rb
94
78
  has_rdoc: true
95
79
  homepage: http://github.com/delano/bluth
96
80
  licenses: []
@@ -105,23 +89,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
105
89
  requirements:
106
90
  - - ">="
107
91
  - !ruby/object:Gem::Version
108
- hash: 3
109
- segments:
110
- - 0
111
92
  version: "0"
112
93
  required_rubygems_version: !ruby/object:Gem::Requirement
113
94
  none: false
114
95
  requirements:
115
96
  - - ">="
116
97
  - !ruby/object:Gem::Version
117
- hash: 3
118
- segments:
119
- - 0
120
98
  version: "0"
121
99
  requirements: []
122
100
 
123
101
  rubyforge_project: bluth
124
- rubygems_version: 1.3.7
102
+ rubygems_version: 1.5.2
125
103
  signing_key:
126
104
  specification_version: 3
127
105
  summary: A Redis queuing system built on top of Familia (w/ daemons!)