There seems to be a quirky behaviour when working with requests without the with
statement as a context manager, which causes resources to be locked up permanently if using the standard if req in res
pattern and both conditions occur simulatenously as follows.
req = resource.request()
result = yield req | env.timeout(5) # req and timeout occurs simultaneously
if req in result:
# DO SOMETHING WITH RESOURCE
resource.release(req)
else:
req.cancel() # Important as req is still in resource queue and can be triggered after timeout, which is another possible cause of a resource being locked permanently
assert not req.triggered # Quirky behaviour here as req can be triggered at this point
After doing further exploration, I found that what is stored within result
is only processed events, so req
could be triggered but not yet processed (although I feel that a request()
being triggered leads to being processed immediately is more intuitive), so we enter the else
block of code. After executing the code within the else
block, the req
would then be processed, but since code execution is already past the if
block, the req
is left dangling and unreleased.
Originally, I thought that req.cancel()
was sufficient but it seems like it does not cancel an event that is triggered (correct me if I'm wrong). As such, the alternative that I came up with was to do this which seemingly fixed the problem.
# Rest of code
else:
req.cancel()
resource.release(req) # Doesn't feel right
(I might be mistaken, so correct me if needed) If req
gets processed later on, the release
or equivalently get
event is scheduled later and will definitely release the resource so it is not left dangling and even if req
was not granted, release(req)
will not cause an exception.
I did not like this approach however. By right, I should end up in the if
block if I do get the resource at the given simulation time, even if the actual processing of req
is "later" on in the queue which reflects the actual behaviour. As such, I tried another method which again seemingly also works ...
# rest of code
if req.triggered:
# rest of code
else:
req.cancel()
# rest of code
So, as the title of this post suggests, is this final method equivalent to doing the standard pattern and thus safe? Would there be any dangers in doing so, since I am technically "skipping time" by treating the req
as processed before it is actually processed (I do not have a case where a resource is used for 0 time and then released, but would be interesting to know if this could be a problem as well)? Perhaps finding a way to re-order the events such that req
is processed first despite being put in the queue later than the timeout event is better? Thank you very much in advance!
Edit: Sample code that demonstrates the behaviour.
import random
import simpy
def source(env, number, counter):
for i in range(number):
c = customer(env, counter)
env.process(c)
yield env.timeout(1)
def customer(env, counter):
patience = 5
while True:
req = counter.request()
results = yield req | env.timeout(patience)
print(f'req triggered = {req.triggered}, req processed = {req.processed}, req in result = {req in results}')
if req in results:
yield env.timeout(5)
counter.release(req)
break
else:
if req.triggered:
assert 0
req.cancel()
random.seed(42)
env = simpy.Environment()
counter = simpy.Resource(env, capacity=1)
env.process(source(env, 100, counter))
env.run(until=100)
You are correct.
There seems to be a edge case where the request is triggered but not processed and expression req in results is false.
However when the event is triggered, the resource is seized, which needs to be released. Also if a event has been triggered, the cancel does nothing.
I looked that context manager for a request and notice it always does a release
So I suggest that the "safe" thing to do if you are not using a with statement is to use a try-finally block, as I did in this code example. If you cannot use a try-finally then you are right, it looks like you should do both a cancel, and a release to "cancel" a request.
Added some logging to show the resource seize / release
import random
import simpy
def source(env, number, counter):
for i in range(number):
c = customer(env, counter, i)
env.process(c)
yield env.timeout(1)
def customer(env, counter, id):
patience = 5
while True:
req=None
try:
to = env.timeout(patience)
req = counter.request()
results = yield req | to
print(env.now,id,f'req triggered = {req.triggered}, req processed = {req.processed}, req in result = {req in results}', (req in req.resource.users), (req in req.resource.queue))
if req in results:
yield env.timeout(5)
break
else:
if req.triggered:
print(id, f'triggered, has resource: {(req in req.resource.users)}, in queue: {(req in req.resource.queue)}')
req.cancel()
finally:
print(id, f'has resource: {(req in req.resource.users)}, in queue: {(req in req.resource.queue)}')
counter.release(req)
print(id, f'has resource: {(req in req.resource.users)}, in queue: {(req in req.resource.queue)}')
random.seed(42)
env = simpy.Environment()
counter = simpy.Resource(env, capacity=1)
env.process(source(env, 100, counter))
env.run(until=50)