Search code examples
pythonsimpy

When yielding anyOf events, is it safe to use `if req.triggered` instead of `if req in res`?


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)

Solution

  • 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)