Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 39aa104

Browse files
authoredJul 25, 2018
Merge 286f77d into 06e9edb
2 parents 06e9edb + 286f77d commit 39aa104

File tree

5 files changed

+352
-2
lines changed

5 files changed

+352
-2
lines changed
 

‎desmod/pool.py

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
"""Pool class for modeling a container of resources.
2+
3+
A pool models a container of items or resources. Pool is similar to the :class:
4+
`simpy.resources.Container`, but with additional events when the Container is
5+
empty or full. Users can put or get items in the pool with a certain amount as
6+
a parameter.
7+
"""
8+
9+
from simpy import Event
10+
from simpy.core import BoundClass
11+
12+
13+
class PoolPutEvent(Event):
14+
def __init__(self, pool, amount=1):
15+
super(PoolPutEvent, self).__init__(pool.env)
16+
self.pool = pool
17+
self.amount = amount
18+
self.callbacks.append(pool._trigger_get)
19+
pool._putters.append(self)
20+
pool._trigger_put()
21+
22+
def cancel(self):
23+
if not self.triggered:
24+
self.pool._putters.remove(self)
25+
self.callbacks = None
26+
27+
28+
class PoolGetEvent(Event):
29+
def __init__(self, pool, amount=1):
30+
super(PoolGetEvent, self).__init__(pool.env)
31+
self.pool = pool
32+
self.amount = amount
33+
self.callbacks.append(pool._trigger_put)
34+
pool._getters.append(self)
35+
pool._trigger_get()
36+
37+
def cancel(self):
38+
if not self.triggered:
39+
self.pool._getters.remove(self)
40+
self.callbacks = None
41+
42+
43+
class PoolWhenNewEvent(Event):
44+
def __init__(self, pool):
45+
super(PoolWhenNewEvent, self).__init__(pool.env)
46+
self.pool = pool
47+
pool._new_waiters.append(self)
48+
pool._trigger_when_new()
49+
50+
def cancel(self):
51+
if not self.triggered:
52+
self.pool._new_waiters.remove(self)
53+
self.callbacks = None
54+
55+
56+
class PoolWhenAnyEvent(Event):
57+
def __init__(self, pool):
58+
super(PoolWhenAnyEvent, self).__init__(pool.env)
59+
self.pool = pool
60+
pool._any_waiters.append(self)
61+
pool._trigger_when_any()
62+
63+
def cancel(self):
64+
if not self.triggered:
65+
self.pool._any_waiters.remove(self)
66+
self.callbacks = None
67+
68+
69+
class PoolWhenFullEvent(Event):
70+
def __init__(self, pool):
71+
super(PoolWhenFullEvent, self).__init__(pool.env)
72+
self.pool = pool
73+
pool._full_waiters.append(self)
74+
pool._trigger_when_full()
75+
76+
def cancel(self):
77+
if not self.triggered:
78+
self.pool._full_waiters.remove(self)
79+
self.callbacks = None
80+
81+
82+
class Pool(object):
83+
"""Simulation pool of arbitrary items.
84+
85+
`Pool` is similar to :class:`simpy.resources.Container`.
86+
It provides a simulation-aware container for managing a pool of objects
87+
needed by multiple processes.
88+
89+
Resources are added and removed using :meth:`put()` and :meth:`get()`.
90+
91+
:param env: Simulation environment.
92+
:param capacity: Capacity of the pool; infinite by default.
93+
:param hard_cap:
94+
If specified, the pool overflows when the `capacity` is reached.
95+
:param init_level: Initial level of the pool.
96+
:param name: Optional name to associate with the queue.
97+
98+
"""
99+
100+
def __init__(self, env, capacity=float('inf'), hard_cap=False,
101+
init_level=0, name=None):
102+
self.env = env
103+
#: Capacity of the queue (maximum number of items).
104+
self.capacity = capacity
105+
self._hard_cap = hard_cap
106+
self.level = init_level
107+
self.name = name
108+
self._putters = []
109+
self._getters = []
110+
self._new_waiters = []
111+
self._any_waiters = []
112+
self._full_waiters = []
113+
self._put_hook = None
114+
self._get_hook = None
115+
BoundClass.bind_early(self)
116+
117+
@property
118+
def size(self):
119+
"""Number of items in pool."""
120+
return self.level
121+
122+
@property
123+
def remaining(self):
124+
"""Remaining pool capacity."""
125+
return self.capacity - self.level
126+
127+
@property
128+
def is_empty(self):
129+
"""Indicates whether the pool is empty."""
130+
return self.level == 0
131+
132+
@property
133+
def is_full(self):
134+
"""Indicates whether the pool is full."""
135+
return self.level >= self.capacity
136+
137+
#: Put amount items in the pool.
138+
put = BoundClass(PoolPutEvent)
139+
140+
#: Get amount items from the queue.
141+
get = BoundClass(PoolGetEvent)
142+
143+
#: Return an event triggered when the pool is non-empty.
144+
when_any = BoundClass(PoolWhenAnyEvent)
145+
146+
#: Return an event triggered when items are put in pool
147+
when_new = BoundClass(PoolWhenNewEvent)
148+
149+
#: Return an event triggered when the pool becomes full.
150+
when_full = BoundClass(PoolWhenFullEvent)
151+
152+
def _add_items(self, item_count=1):
153+
self.level += item_count
154+
155+
def _remove_items(self, item_count=1):
156+
self.level -= item_count
157+
return item_count
158+
159+
def _trigger_put(self, _=None):
160+
if self._putters:
161+
put_ev = self._putters.pop(0)
162+
put_ev.succeed()
163+
self._add_items(put_ev.amount)
164+
self._trigger_when_new()
165+
self._trigger_when_any()
166+
self._trigger_when_full()
167+
if self._put_hook:
168+
self._put_hook()
169+
if self.level > self.capacity and self._hard_cap:
170+
raise OverflowError()
171+
172+
def _trigger_get(self, _=None):
173+
if self._getters and self.level:
174+
for index, get_ev in enumerate(self._getters):
175+
get_ev = self._getters[index]
176+
assert get_ev.amount <= self.capacity, (
177+
"Amount {} greater than pool's {} capacity {}".format(
178+
get_ev.amount, str(self.name), self.capacity))
179+
if get_ev.amount <= self.level:
180+
self._getters.remove(get_ev)
181+
item = self._remove_items(get_ev.amount)
182+
get_ev.succeed(item)
183+
if self._get_hook:
184+
self._get_hook()
185+
break
186+
187+
def _trigger_when_new(self):
188+
for when_new_ev in self._new_waiters:
189+
when_new_ev.succeed()
190+
del self._new_waiters[:]
191+
192+
def _trigger_when_any(self):
193+
if self.level:
194+
for when_any_ev in self._any_waiters:
195+
when_any_ev.succeed()
196+
del self._any_waiters[:]
197+
198+
def _trigger_when_full(self):
199+
if self.level >= self.capacity:
200+
for when_full_ev in self._full_waiters:
201+
when_full_ev.succeed()
202+
del self._full_waiters[:]
203+
204+
def __str__(self):
205+
return ('Pool: name={0.name}'
206+
' level={0.level}'
207+
' capacity={0.capacity}'
208+
')'.format(self))

‎desmod/probe.py

+22
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import six
55

66
from desmod.queue import Queue
7+
from desmod.pool import Pool
78

89

910
def attach(scope, target, callbacks, **hints):
@@ -23,6 +24,11 @@ def attach(scope, target, callbacks, **hints):
2324
_attach_queue_remaining(target, callbacks)
2425
else:
2526
_attach_queue_size(target, callbacks)
27+
elif isinstance(target, Pool):
28+
if hints.get('trace_remaining', False):
29+
_attach_pool_remaining(target, callbacks)
30+
else:
31+
_attach_pool_size(target, callbacks)
2632
else:
2733
raise TypeError(
2834
'Cannot probe {} of type {}'.format(scope, type(target)))
@@ -124,3 +130,19 @@ def hook():
124130
callback(queue.remaining)
125131

126132
queue._put_hook = queue._get_hook = hook
133+
134+
135+
def _attach_pool_size(pool, callbacks):
136+
def hook():
137+
for callback in callbacks:
138+
callback(pool.size)
139+
140+
pool._put_hook = pool._get_hook = hook
141+
142+
143+
def _attach_pool_remaining(pool, callbacks):
144+
def hook():
145+
for callback in callbacks:
146+
callback(pool.remaining)
147+
148+
pool._put_hook = pool._get_hook = hook

‎desmod/tracer.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .util import partial_format
1313
from .timescale import parse_time, scale_time
1414
from .queue import Queue
15+
from .pool import Pool
1516

1617

1718
class Tracer(object):
@@ -205,7 +206,7 @@ def activate_probe(self, scope, target, **hints):
205206
assert self.enabled
206207
var_type = hints.get('var_type')
207208
if var_type is None:
208-
if isinstance(target, simpy.Container):
209+
if isinstance(target, (simpy.Container, Pool)):
209210
if isinstance(target.level, float):
210211
var_type = 'real'
211212
else:
@@ -221,7 +222,7 @@ def activate_probe(self, scope, target, **hints):
221222
if k in hints}
222223

223224
if 'init' not in kwargs:
224-
if isinstance(target, simpy.Container):
225+
if isinstance(target, (simpy.Container, Pool)):
225226
kwargs['init'] = target.level
226227
elif isinstance(target, simpy.Resource):
227228
kwargs['init'] = len(target.users) if target.users else 'z'

‎tests/test_pool.py

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from pytest import raises
2+
3+
from desmod.pool import Pool
4+
5+
6+
def test_pool(env):
7+
pool = Pool(env, capacity=2)
8+
9+
def producer(amount, wait):
10+
yield env.timeout(wait)
11+
yield pool.put(amount)
12+
13+
def consumer(expected_amount, wait):
14+
yield env.timeout(wait)
15+
msg = yield pool.get(expected_amount)
16+
assert msg == expected_amount
17+
18+
env.process(producer(1, 0))
19+
env.process(producer(2, 1))
20+
env.process(consumer(1, 0))
21+
env.process(consumer(2, 1))
22+
env.run()
23+
24+
25+
def test_pool_when_full_any(env):
26+
pool = Pool(env, capacity=9)
27+
result = []
28+
29+
def producer(env):
30+
yield env.timeout(1)
31+
for i in range(1, 6):
32+
yield pool.put(i)
33+
yield env.timeout(1)
34+
35+
def consumer(env):
36+
yield env.timeout(5)
37+
for i in range(1, 3):
38+
msg = yield pool.get(i)
39+
assert msg == i
40+
41+
def full_waiter(env):
42+
yield pool.when_full()
43+
assert env.now == 4
44+
assert pool.level == 10
45+
result.append('full')
46+
47+
def any_waiter(env):
48+
yield pool.when_any()
49+
assert env.now == 1
50+
result.append('any')
51+
52+
env.process(producer(env))
53+
env.process(consumer(env))
54+
env.process(full_waiter(env))
55+
env.process(any_waiter(env))
56+
env.process(any_waiter(env))
57+
env.run()
58+
assert pool.level
59+
assert pool.is_full
60+
assert pool.remaining == pool.capacity - pool.level
61+
assert not pool.is_empty
62+
assert pool.size == 12
63+
assert 'full' in result
64+
assert result.count('any') == 2
65+
66+
67+
def test_pool_overflow(env):
68+
pool = Pool(env, capacity=5, hard_cap=True)
69+
70+
def producer(env):
71+
yield env.timeout(1)
72+
for i in range(5):
73+
yield pool.put(i)
74+
yield env.timeout(1)
75+
76+
env.process(producer(env))
77+
with raises(OverflowError):
78+
env.run()
79+
80+
81+
def test_pool_get_more(env):
82+
pool = Pool(env, capacity=6, name='foo')
83+
84+
def producer(env):
85+
yield pool.put(1)
86+
yield env.timeout(1)
87+
yield pool.put(1)
88+
89+
def consumer(env, amount1, amount2):
90+
amount = yield pool.get(amount1)
91+
assert amount == amount1
92+
amount = yield pool.get(amount2) # should fail
93+
94+
env.process(producer(env))
95+
env.process(consumer(env, 1, 10))
96+
with raises(AssertionError,
97+
message="Amount {} greater than pool's {} capacity {}".format(
98+
10, 'foo', 6)):
99+
env.run()
100+
101+
102+
def test_pool_cancel(env):
103+
pool = Pool(env)
104+
105+
event_cancel = pool.get(2)
106+
event_cancel.cancel()
107+
event_full = pool.when_full()
108+
event_full.cancel()
109+
event_any = pool.when_any()
110+
event_any.cancel()
111+
112+
env.run()
113+
assert pool.level == 0
114+
assert not event_cancel.triggered
115+
assert not event_full.triggered
116+
assert not event_any.triggered

‎tests/test_tracer.py

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from desmod.component import Component
88
from desmod.queue import Queue
9+
from desmod.pool import Pool
910
from desmod.simulation import simulate
1011

1112
pytestmark = pytest.mark.usefixtures('cleandir')
@@ -50,6 +51,7 @@ def __init__(self, *args, **kwargs):
5051
self.container = simpy.Container(self.env)
5152
self.resource = simpy.Resource(self.env)
5253
self.queue = Queue(self.env)
54+
self.pool = Pool(self.env)
5355
self.a = CompA(self)
5456
self.b = CompB(self)
5557
hints = {}
@@ -62,6 +64,7 @@ def __init__(self, *args, **kwargs):
6264
self.auto_probe('container', **hints)
6365
self.auto_probe('resource', **hints)
6466
self.auto_probe('queue', **hints)
67+
self.auto_probe('pool', **hints)
6568
self.trace_some = self.get_trace_function(
6669
'something', vcd={'var_type': 'real'}, log={'level': 'INFO'})
6770
self.trace_other = self.get_trace_function(

0 commit comments

Comments
 (0)
Please sign in to comment.