4

I have simplified the code representing the problem as suggested and attempted to debug.

Here is the most simple version of the code which still contains the bug:

import simpy

SIM_TIME = 5

def source(env):    
    i = 0
    while True:
        i += 1
        env.process(train(env, 'Train %d' % i, 'a'))
        yield env.timeout(1)
        i += 1
        env.process(train(env, 'Train %d' % i, 'b'))
        yield env.timeout(1)

def train(env, name, route):
    resource_req = resource.request()
    p3_req = p3.request()

    if route == 'a':
        print 'Route a %s generated' % name
        yield resource_req
        print 'Route a %s seized resource at time %d' % (name, env.now)
        yield env.timeout(1)
        resource.release(resource_req) 
        print 'Route a %s released resource at time %d' % (name, env.now)

    elif route == 'b':
        print 'Route b %s generated' % name
        yield p3_req
        print 'Route b %s seized resource at time %d' % (name, env.now)
        yield env.timeout(1)
        p3.release(p3_req)
        print 'Route b %s released resource at time %d' % (name, env.now)

    else:
        print 'Could not find a route branch'

env = simpy.Environment()

resource = simpy.Resource(env, 1)
p3 = simpy.Resource(env, 1)

env.process(source(env))
env.run(until=SIM_TIME)

The output from running this:

Route a Train 1 generated
Route a Train 1 seized resource at time 0
Route b Train 2 generated
Route a Train 1 released resource at time 1
Route a Train 3 generated
Route b Train 4 generated
Route a Train 5 generated

As you can see the resource is apparently released on "route a" but then no other resources can be seized either on route a or route b.

I'm very confused by this. Any help would be much appreciated!

Harry Munro
  • 304
  • 2
  • 12
  • Have you tried printing a message to check if you ever hit the `else` branch (that you left out)? If you would hit it, you would not release the resource. – Stefan Scherfke Sep 08 '16 at 20:27
  • Yeah just checked else statements for both routes and they're never hit - the logic is ok. Strange that the resource is not being released. – Harry Munro Sep 09 '16 at 07:31
  • Then please try to find a minimal example where you sitll hit the bug. Remove code line by line and test it. If the bug does not appear, re-insert the last deletion and try to remove something else until there’s nothing more that you can remove. – Stefan Scherfke Sep 09 '16 at 20:42
  • Thanks Stefan, I've reduced the problem as much as possible while still containing the bug. For some reason after the first resource is released, no resources can be seized. – Harry Munro Sep 12 '16 at 11:12
  • @StefanScherfke, I added what I think is a minimal working example in a solution below. I think this explains the issue, and I wondered what your view of the most correct solution was. – Dr Xorile Oct 31 '16 at 22:56

2 Answers2

3

I believe the problem comes about because the release refers to a specific request, but the request has by then already be over-written by a new request. resource_req is the request, but before it can be released, it is overwritten by a new resource_req. I believe that when you try to release this new request it does not release it properly, because it's not a release that is being handled by the resource (it's the new one).

I don't know what the solution is. I came across this post trying to find it, because I'm having the same problem myself. One obvious possibility (which I haven't tried yet) is to create a list of requests, and keep track of them, but this seems like a silly solution. There must be a way to simply free up the resource (which is the behaviour that is desired). I'll try to post back if I figure it out!

Here's a minimal working example:

import simpy

class Machine:
    def __init__(self,env):
        self.machine = simpy.Resource(env,capacity=1)
        self.load_proc = env.process(self.load(env))

    def load(self,env):
        "Load machine 1 when it's empty"
        while True:
            self.req = self.machine.request()
            print("Waiting for machine at %d" %env.now)
            yield self.req
            print("Load machine at %d" %env.now)
            self.process_proc = env.process(self.process(env))

    def process(self,env):
        "Machine does process and is then emptied"
        print("Machine starts process at %d" %env.now)
        yield env.timeout(10)
        print("Machine finished process at %d" %env.now)
        self.machine.release(self.req)
        print("Machine released at %d" %env.now)

env = simpy.Environment()
M1 = Machine(env)
env.run(until=100)

Here there is a machine that continually tries to load, but waits for it to be empty. Once it's loaded, it tries to run a 10 second process and then releases the Resource to allow it to be loaded again. In 100 time-steps it should obviously be able to make 10 batches, but only the first one is done.

>>> 
Waiting for machine at 0
Load machine at 0
Waiting for machine at 0
Machine starts process at 0
Machine finished process at 10
Machine released at 10
>>> 

It seems that the release does not work, because it is referring to the second request. Diagnosing this makes it possible to find a workaround, but it would be nice to know the right way to do it!


One possible solution is to just release a current user rather than a specific request:

import simpy

class Machine:
    def __init__(self,env):
        self.machine = simpy.Resource(env,capacity=1)
        self.load_proc = env.process(self.load(env))

    def load(self,env):
        "Load machine 1 when it's empty"
        while True:
            print("Waiting for machine at %d" %env.now)
            yield self.machine.request()
            print("Load machine at %d" %env.now)
            self.process_proc = env.process(self.process(env))

    def process(self,env):
        "Machine does process and is then emptied"
        print("Machine starts process at %d" %env.now)
        yield env.timeout(10)
        print("Machine finished process at %d" %env.now)
        if len(self.machine.users)>=1: self.machine.release(self.machine.users[0])
        print("Machine released at %d" %env.now)

env = simpy.Environment()
M1 = Machine(env)
env.run(until=100)

This behaves as expected, and has the advantage that you don't need a variable for the request. The release of

self.machine.release(self.machine.users[0])

is probably sufficient, unless you're at risk of releasing something that hasn't been requested already.


Update based on Stefan Scherfke's comment, is to pass the req explicitly to the new process:

import simpy

class Machine:
    def __init__(self,env):
        self.machine = simpy.Resource(env,capacity=1)
        self.load_proc = env.process(self.load(env))

    def load(self,env):
        "Load machine 1 when it's empty"
        while True:
            print("Waiting for machine at %d" %env.now)
            req = self.machine.request()
            yield req
            print("Load machine at %d" %env.now)
            self.process_proc = env.process(self.process(env,req))

    def process(self,env,req):
        "Machine does process and is then emptied"
        print("Machine starts process at %d" %env.now)
        yield env.timeout(10)
        print("Machine finished process at %d" %env.now)
        self.machine.release(req)
        print("Machine released at %d" %env.now)

env = simpy.Environment()
M1 = Machine(env)
env.run(until=100)

This does indeed work as expected.

Community
  • 1
  • 1
Dr Xorile
  • 967
  • 1
  • 7
  • 20
  • Maybe @Stefan Scherfke can suggest a better solution! – Dr Xorile Oct 28 '16 at 17:29
  • What is the purpose of `self.process_proc`? If that line of code is just where you want the `Machine` to do its "work" (as defined in the `process` method) then you can replace ``self.process_proc = env.process(self.process(env))`` with ``yield env.process(self.process(env))`` – Eric La Fevers Nov 03 '16 at 22:03
  • @EricLaFevers, this is just to demonstrate the specific issue of the resource release, when the variable that you are releasing is over-written. This is a very simple simulation that could be done in much better ways, I'm sure. It's a minimal example of a common user error. Normally, when making a simulation, you want to follow the logic of the actual thing you're simulating closely, so it's quite possible that you'd want to just kick off a secondary process and carry on, rather than wait for the secondary process to finish. – Dr Xorile Nov 03 '16 at 23:17
  • The safest way of requesting a resource is using the `with` statement. When you need to release it in another process (as in your example), you should pass the request explicitly to the process instead of setting it as an instance attribute. This way, the request cannot be overwritten by another process. – Stefan Scherfke Nov 04 '16 at 17:19
0

Solved - there was a typo in the resource.

Originally defined as:

resource = simpy.Resource(env, 1)

Correct definition:

resource = simpy.Resource(env, capacity = 1)
Harry Munro
  • 304
  • 2
  • 12