Skip to content

Commit 8a77ecc

Browse files
committed
Merge branch 'stable'
Conflicts: garlicsim/garlicsim/asynchronous_crunching/crunching_manager.py
2 parents ae74b1a + 2dbad18 commit 8a77ecc

File tree

2 files changed

+362
-1
lines changed

2 files changed

+362
-1
lines changed
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
# Copyright 2009-2011 Ram Rachum.
2+
# This program is distributed under the LGPL2.1 license.
3+
4+
'''
5+
This module defines the `CrunchingManager` class.
6+
7+
See its documentation for more information.
8+
'''
9+
10+
from __future__ import with_statement
11+
12+
from garlicsim.general_misc import queue_tools
13+
from garlicsim.general_misc import decorator_tools
14+
import garlicsim.general_misc.change_tracker
15+
from garlicsim.general_misc.infinity import infinity
16+
from garlicsim.general_misc import misc_tools
17+
from garlicsim.general_misc import address_tools
18+
from garlicsim.general_misc import cute_iter_tools
19+
20+
import garlicsim
21+
import garlicsim.data_structures
22+
import garlicsim.misc
23+
from . import crunchers
24+
from .crunching_profile import CrunchingProfile
25+
from .base_cruncher import BaseCruncher
26+
from garlicsim.misc.step_profile import StepProfile
27+
from .misc import EndMarker
28+
29+
30+
__all__ = ['CrunchingManager']
31+
32+
33+
@decorator_tools.decorator
34+
def with_tree_lock(method, *args, **kwargs):
35+
'''
36+
Decorator for using the tree lock (in write mode) as a context manager.
37+
'''
38+
self = args[0]
39+
with self.project.tree.lock.write:
40+
return method(*args, **kwargs)
41+
42+
43+
class CrunchingManager(object):
44+
'''
45+
A crunching manager manages the background crunching for a project.
46+
47+
Every project creates a crunching manager. The job of the crunching manager
48+
is to coordinate the crunchers, creating and retiring them as necessary.
49+
The main use of a crunching manager is through its sync_workers methods,
50+
which goes over all the crunchers and all the nodes of the tree that need
51+
to be crunched, making sure the crunchers are working on these nodes, and
52+
collecting work from them to implement into the tree.
53+
54+
The crunching manager contains a list of jobs as an attribute `.jobs`. See
55+
documentation for garlicsim.asynchronous_crunching.Job for more info about
56+
jobs. The crunching manager will employ crunchers in order to complete the
57+
jobs. It will then take work from these crunchers, put it into the tree,
58+
and delete the jobs when they are completed.
59+
'''
60+
61+
def __init__(self, project):
62+
63+
self.project = project
64+
65+
self.jobs = []
66+
'''
67+
The jobs that the crunching manager will be responsible for doing.
68+
69+
These are of the class `garlicsim.asynchronous_crunching.Job`.
70+
'''
71+
72+
self.crunchers = {}
73+
'''Dict that maps each job to the cruncher reponsible for doing it.'''
74+
75+
self.step_profiles = {}
76+
'''
77+
Dict that maps each cruncher to its step options profile.
78+
79+
This exists because if the step profile for a job changes, we need to
80+
retire the cruncher and make a new one; crunchers can't change step
81+
profiles on the fly. So we use this dict to track which step profile
82+
each cruncher uses.
83+
'''
84+
85+
self.crunching_profiles_change_tracker = \
86+
garlicsim.general_misc.change_tracker.ChangeTracker()
87+
'''
88+
A change tracker which tracks changes made to crunching profiles.
89+
90+
This is used to update the crunchers if the crunching profile for the
91+
job they're working on has been changed.
92+
'''
93+
94+
available_cruncher_types = \
95+
self.project.simpack_grokker.available_cruncher_types
96+
97+
if not available_cruncher_types:
98+
raise garlicsim.misc.GarlicSimException(
99+
"The `%s` simpack doesn't allow using any of the cruncher "
100+
"types we have installed." % self.project.simpack.__name__
101+
)
102+
103+
104+
self.cruncher_type = available_cruncher_types[0]
105+
'''
106+
The cruncher type that we will use to crunch the simulation.
107+
108+
All crunchers that the crunching manager will create will be of this
109+
type. The user may assign a different cruncher type to
110+
`.cruncher_type`, and on the next call to `.sync_crunchers` the
111+
crunching manager will retire all the existing crunchers and replace
112+
them with crunchers of the new type.
113+
'''
114+
115+
116+
@with_tree_lock
117+
def sync_crunchers(self):
118+
'''
119+
Take work from the crunchers, and give them new instructions if needed.
120+
121+
Talks with all the crunchers, takes work from them for implementing
122+
into the tree, retiring crunchers or recruiting new crunchers as
123+
necessary.
124+
125+
Returns the total amount of nodes that were added to the tree in the
126+
process.
127+
'''
128+
# This is one of the most technical and sensitive functions in all of
129+
# GarlicSim-land. Be careful if you're trying to make changes.
130+
131+
total_added_nodes = garlicsim.misc.NodesAdded(0)
132+
'''int-oid in which we track the number of nodes added to the tree.'''
133+
134+
# The first thing we do is iterate over the crunchers whose jobs have
135+
# been terminated. We take work from them, put it into the tree, and
136+
# promptly retire them, deleting them from `self.crunchers`.
137+
138+
for (job, cruncher) in self.crunchers.copy().items():
139+
if not (job in self.jobs):
140+
(added_nodes, new_leaf) = \
141+
self.__add_work_to_tree(cruncher, job, retire=True)
142+
total_added_nodes += added_nodes
143+
del self.crunchers[job]
144+
145+
146+
# In this point all the crunchers in `.crunchers` have an active job
147+
# associated with them.
148+
#
149+
# Now we'll iterate over the active jobs.
150+
151+
for job in self.jobs[:]:
152+
153+
154+
if job not in self.crunchers:
155+
156+
# If there is no cruncher associated with the job, we create
157+
# one. (As long as the job is unfinished, and the node isn't in
158+
# editing.) And that's it for this job, we `continue` to the
159+
# next one.
160+
161+
if not job.is_done():
162+
self.__conditional_create_cruncher(job)
163+
else: # job.is_done() is True
164+
self.jobs.remove(job)
165+
continue
166+
167+
# job in self.crunchers
168+
#
169+
# Okay, so it's an active job. We'll take work from the cruncher and
170+
# put it in the tree, updating the job to point at `new_leaf`, which
171+
# is the node (leaf) containing the most recent state produced by
172+
# the cruncher.
173+
#
174+
# The cruncher may either be active and crunching, or it may have
175+
# stopped, (because of a `WorldEnded` exception, or other reasons.)
176+
177+
cruncher = self.crunchers[job]
178+
179+
(added_nodes, new_leaf) = self.__add_work_to_tree(cruncher,
180+
job)
181+
total_added_nodes += added_nodes
182+
183+
job.node = new_leaf
184+
185+
# We took work from the cruncher, now it's time to decide if we want
186+
# the cruncher to keep running or not. We will also update its
187+
# crunching profile, if that has been changed on the job.
188+
189+
if not job.is_done():
190+
191+
# (We have called `job.is_done` again because the job's node may
192+
# have changed, and possibly the new node *does* satisfy the
193+
# job's crunching profile that the previous node didn't.)
194+
195+
crunching_profile = job.crunching_profile
196+
197+
if cruncher.is_alive() and \
198+
(type(cruncher) is self.cruncher_type):
199+
200+
# The job is not done, the cruncher's still working and it
201+
# is of the right type. In this case, the only thing left to
202+
# do is check if the crunching profile changed.
203+
204+
# First we'll check if the step profile changed:
205+
206+
if crunching_profile.step_profile != \
207+
self.step_profiles[cruncher]:
208+
209+
# If it did, we immediately replace the cruncher,
210+
# because crunchers can't change step profile on the
211+
# fly.
212+
213+
if cruncher.is_alive():
214+
cruncher.retire()
215+
216+
self.__conditional_create_cruncher(job)
217+
218+
continue
219+
220+
221+
# At this point we know that the step profile hasn't
222+
# changed, but possibly some other part (i.e. clock target)
223+
# has changed, and if so we update the cruncher about it.
224+
225+
if self.crunching_profiles_change_tracker.check_in \
226+
(crunching_profile):
227+
228+
cruncher.update_crunching_profile(crunching_profile)
229+
230+
continue
231+
232+
else:
233+
234+
# Either the cruncher died, or it is of the wrong type. The
235+
# latter happens when the user changes
236+
# `crunching_manager.cruncher_type` in the middle of
237+
# simulating. In any case, this cruncher is done for.
238+
239+
cruncher.retire() # In case it's not totally dead.
240+
241+
self.__conditional_create_cruncher(job)
242+
243+
continue
244+
245+
246+
else: # job.is_done() is True
247+
248+
# The job is done; we remove it from the job list, and retire
249+
# and delete the cruncher.
250+
251+
self.jobs.remove(job)
252+
if cruncher.is_alive():
253+
cruncher.retire()
254+
del self.crunchers[job]
255+
256+
257+
return total_added_nodes
258+
259+
260+
261+
def __conditional_create_cruncher(self, job):
262+
'''
263+
Create a cruncher to crunch the node, unless there is reason not to.
264+
'''
265+
node = job.node
266+
crunching_profile = job.crunching_profile
267+
268+
if node.still_in_editing is False:
269+
cruncher = self.cruncher_type(self, node.state, crunching_profile)
270+
cruncher.start()
271+
self.crunchers[job] = cruncher
272+
273+
self.crunching_profiles_change_tracker.check_in(crunching_profile)
274+
self.step_profiles[cruncher] = \
275+
crunching_profile.step_profile
276+
277+
278+
def get_jobs_by_node(self, node):
279+
'''
280+
Get all the jobs that should be done on the specified node.
281+
282+
This is every job whose `.node` attribute is the given node/
283+
'''
284+
return [job for job in self.jobs if (job.node is node)]
285+
286+
287+
def __add_work_to_tree(self, cruncher, job, retire=False):
288+
'''
289+
Take work from cruncher and add to tree at the specified job's node.
290+
291+
If `retire` is set to `True`, retires the cruncher. Keep in mind that
292+
if the cruncher gives an `EndMarker`, it will be retired regardless of
293+
the `retire` argument.
294+
295+
Returns `(number, leaf)`, where `number` is the number of nodes that
296+
were added, and `leaf` is the last node that was added.
297+
'''
298+
299+
tree = self.project.tree
300+
node = job.node
301+
302+
current_node = node
303+
counter = 0
304+
305+
queue_iterator = queue_tools.iterate(
306+
cruncher.work_queue,
307+
limit_to_original_size=True,
308+
_prefetch_if_no_qsize=True
309+
)
310+
311+
for thing in queue_iterator:
312+
313+
if isinstance(thing, garlicsim.data_structures.State):
314+
counter += 1
315+
current_node = tree.add_state(
316+
thing,
317+
parent=current_node,
318+
step_profile=self.step_profiles[cruncher],
319+
)
320+
# todo optimization: save step profile in variable, it's
321+
# wasteful to do a dict lookup every state.
322+
323+
elif isinstance(thing, EndMarker):
324+
tree.make_end(node=current_node,
325+
step_profile=self.step_profiles[cruncher])
326+
job.resulted_in_end = True
327+
328+
else:
329+
raise TypeError('Unexpected object `%s` in work queue' % thing)
330+
331+
if retire or job.resulted_in_end:
332+
cruncher.retire()
333+
334+
nodes_added = garlicsim.misc.NodesAdded(counter)
335+
336+
return (nodes_added, current_node)
337+
338+
339+
def __repr__(self):
340+
'''
341+
Get a string representation of the crunching manager.
342+
343+
Example output:
344+
<garlicsim.asynchronous_crunching.CrunchingManager
345+
currently employing 2 crunchers to handle 2 jobs at 0x1f699b0>
346+
'''
347+
348+
crunchers_count = len(self.crunchers)
349+
job_count = len(self.jobs)
350+
351+
return (
352+
'<%s currently employing %s crunchers to handle %s jobs at %s>' %
353+
(
354+
address_tools.describe(type(self), shorten=True),
355+
crunchers_count,
356+
job_count,
357+
hex(id(self))
358+
)
359+
)
360+

python_toolbox/context_managers/reentrant_context_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
from garlicsim.general_misc.proxy_property import ProxyProperty
1414

1515
from .context_manager import ContextManager
16-
16+
# blocktodo: need tests on whether `depth` is increased before or after
17+
# `__enter__` and `__exit__`. Was relevant in freezers.
1718

1819
class ReentrantContextManager(ContextManager):
1920
'''

0 commit comments

Comments
 (0)