0

I would like to create a system with servers which need time to set-up before being ready to serve. A server is set up whenever there is a customer arriving to the queue, and the the earlier coming customer will seize the server which is ON earlier, like below.

  1. Customer 1 arrives and requests a server.
  2. Server 1 is SETUP in t1 secs.
  3. Customer 2 arrives and requests a server.
  4. Server 2 is SETUP in t2 secs.
  5. Server 2 is ON.
  6. Customer 1 occupies Server 2.

This process has been successfully simulated thanks to the great answer here: Simulating a system of resource with set-up/switch-on times using Simpy

I would like to add another policy to the system. When a customer leaves the system, he checks if there is any other customer who is waiting to be served. If so, he keeps the server remaining ON, otherwise, he turns off the server immediatelty.

  1. Customer 1 completes the service and leaves the system.
  2. Server 2 remains ON.

Customer 2 sees that Server 2 is ON before Server 1, and sees that no one else is waiting, so he turns off Server 1.

  1. Customer 2 occupies Server 2.
  2. Server 1 (still in SETUP mode) is turned off.

So, 7),8),9),10) happen at the same time, and the occurence of Event 7) triggers the Interuption of the ealier Event 2) in 10).

Is it possible to manage such kind of Interruption in the Server_Management() class?

"""
Simulation of a dynamic server pool

Server pool starts empty and servers are added as needed, but there is a delay 
simulating start up time before the server is available to fulfill a resource request

Programmer: Matt
    Wrote original version

Programmer: Michael R. Gibbs
    Added server check for dynamicaly adding servers
    Fixed return of resorces to pool
"""

import simpy

LAM = 8  #arival rate of jobs
MU = 2  #service rate
ALPHA = 12  #set up rate
NUM_SERVERS = 3
MAX_NUM_JOB = 10000000000
UNTIL = 5

num_current_jobs = 0  #global variable which record the number of jobs present in the system

def generate_interarrival():
  return np.random.exponential(1/LAM)

def generate_service():
  return np.random.exponential(1/MU)

def switch_on():
  return np.random.exponential(1/ALPHA)

class Generate_Job(): 
    def arriving_job(env, servers):
        global  num_current_jobs, num_server_on, leaving_time_list
        for i in range(MAX_NUM_JOB):
            job = Job(name="Job%01d" % (i))
            yield env.timeout(generate_interarrival())
            print('{0:.5f}'.format(env.now), job.name, "arrives")
            num_current_jobs +=1
            env.process(job.handling(env,servers))

class Server_Management():
    def check_servers_arriving(env, servers):
        global  num_current_jobs
        """
        Checks the server pool to see if the pool has any avalable servers
        if not then add a server, (there will be a delay before added server becomes available)

        Call this without a yield so it does not block if a server is added
        """

        print('{0:.5f}'.format(env.now), "checking #OFF servers:",max(0,NUM_SERVERS-num_current_jobs+1))
        
        if num_current_jobs <= NUM_SERVERS:
            # will need another server
            switch_on_time = switch_on()
            print('{0:.5f}'.format(env.now), "adding a server at " + '{0:.5f}'.format(env.now + switch_on_time) + " --")
            yield env.timeout(switch_on_time)    #switch on time
            yield servers.put(1)     
            print('{0:.5f}'.format(env.now), "added a server--")

    def check_servers_leaving(env, servers):
        global  num_current_jobs
                """
        Checks the queue to see if there is any customer is waiting
        """
        print('{0:.5f}'.format(env.now), "checking queue length:",max(0,num_current_jobs-NUM_SERVERS))

        if num_current_jobs >= NUM_SERVERS:  #if there is any waiting customer
            yield servers.put(1)             #Keep the computer remain ON
            print('{0:.5f}'.format(env.now), "computer remains ON--")


class Room:                             # A room containing servers (resource)
    def __init__(self, env):
        self.computer = simpy.Container(env, capacity = NUM_SERVERS, init = 0)

class Job(object):
    def __init__(self,name):
        self.name = name
    
    def handling(self, env, servers):
        global num_current_jobs, num_server_on, job_list
        # added a check to see if a resource pool needs another server.
        env.process(Server_Management.check_servers_arriving(env,servers.computer))
        print('{0:.5f}'.format(env.now), self.name, "requesting a server--") 
        with servers.computer.get(1) as req:
            yield req 
            print('{0:.5f}'.format(env.now), self.name, "occupies a server--")        
            yield env.timeout(generate_service())    #service time
            print('{0:.5f}'.format(env.now), self.name, "leaves")
            num_current_jobs -= 1
        # added a check to see if a resource pool needs another server.
        env.process(Server_Management.check_servers_leaving(env,servers.computer))

np.random.seed(0)
env = simpy.Environment()
servers = Room(env)
env.process(Generate_Job.arriving_job(env, servers))
env.run(until = UNTIL) 
Matt
  • 5
  • 3

2 Answers2

1

got curious and coded it out, I need to go dark for a little while and get some paying work done

"""
Simulation of a dynamic server pool

Server pool starts empty and servers are added as needed, but there is a delay 
simulating start up time before the server is available to fulfill a resource request
After a server is started a check is made to see if the server is still needed before
it is added to the resouce pool

Programmer: Matt
    Wrote original version

Programmer: Michael R. Gibbs
    Added server check for dynamicaly adding servers
    servers are returned to resouce pool only if needed (get queue size > 0)
"""

import simpy
import numpy as np

LAM = 8  #arival rate of jobs
MU = 2  #service rate
ALPHA = 12  #set up rate
NUM_SERVERS = 5
MAX_NUM_JOB = 50 #10000000000
UNTIL = 10

server_cnt = 0
job_cnt = 0
start_up_list = []

def generate_interarrival():
  return np.random.exponential(1/LAM)

def generate_service():
  return np.random.exponential(1/MU)

def switch_on():
  return np.random.exponential(1/ALPHA)

def return_server(env,servers):
    """
    checks if the server is still needed,
    if so add back to the resource pool so waiting request can be filled
    else, do not add back to resource pool simulating shutdown
    """

    global server_cnt, start_up_list, job_cnt

    if len(servers.get_queue) > 0:
        # server is still needed
        yield servers.put(1)
        print('{0:.5f}'.format(env.now), "queuing server --")

        if server_cnt > job_cnt: 
            # have a extra server, try to kill starting up server

            # first clean up events that have already happend
            i = len(start_up_list)-1
            while i >= 0:
                e = start_up_list[i]
                if e.triggered:
                    start_up_list.pop(i)
                i -=1

            # kill last added startup process hoping that is the one with longest time before start up finishes
            if len(start_up_list) > 0:
                e = start_up_list.pop()
                e.interrupt()
                print('{0:.5f}'.format(env.now), "killing start up server --------------------------------")
    else:
        print('{0:.5f}'.format(env.now), "shutting down server --")
        server_cnt -= 1

def check_servers(env, servers):
    """
    Checks the server pool to see if the pool has any avalable servers
    if not then add a server, (there will be a delay before added server becomes available)

    after the start up delay, check again to see if the server is still needed

    Call this without a yield so it does not block if a server is added
    """

    global server_cnt

    print('{0:.5f}'.format(env.now), "checking server pool", "requests:",len(servers.get_queue), "idel:", servers.level, "servers:", server_cnt)
    
    if len(servers.get_queue) >= servers.level and server_cnt < NUM_SERVERS:
        # will need another server
        server_cnt += 1
        d = switch_on()
        startT = env.now + d

        print('{0:.5f}'.format(env.now), "adding a server at " + '{0:.5f}'.format(startT) + " --")
        
        try: # catch interrupts exceptions
            # start up
            yield env.timeout(d)    #switch on time

            # check if server is still needed
            if len(servers.get_queue) > 0:
                # still need it so add
                yield servers.put(1)      
                print('{0:.5f}'.format(env.now), "added a server--")
            else:
                print('{0:.5f}'.format(env.now), "server not needed, not added--")
                server_cnt -=1
        except:
            server_cnt -= 1
            print('{0:.5f}'.format(env.now), "server starting at " + '{0:.5f}'.format(startT) + " has been killed --")

class Generate_Job(): 
    def arriving_job(env, servers):
        global  num_current_jobs, num_server_on, leaving_time_list
        for i in range(MAX_NUM_JOB):
            job = Job(name="Job%01d" % (i))
            yield env.timeout(generate_interarrival())
            print('{0:.5f}'.format(env.now), job.name, "arrives")

            env.process(job.handling(env,servers))

class Room:                             # A room containing servers (resource)
    def __init__(self, env):
        self.computer = simpy.Container(env, capacity = 10000, init = 0)


class Job(object):
    def __init__(self,name):
        self.name = name

    def handling(self, env, servers):

        global start_up_list, job_cnt

        # added a check to see if a resource pool needs another server.
        job_cnt += 1

        start_evt = env.process(check_servers(env,servers.computer))
        start_up_list.append(start_evt)
        print('{0:.5f}'.format(env.now), self.name, "requesting a server--") 

        with servers.computer.get(1) as req: 
            
            yield req 
            # if the queue is empty then the req is never filled and the next lines are never called
            # need to do this before the rescource requests
            #                       
            # yield env.timeout(switch_on())    #switch on time
            # yield servers.server.put(1)            
            print('{0:.5f}'.format(env.now), self.name, "occupies a server--")        
            yield env.timeout(generate_service())    #service time
            print('{0:.5f}'.format(env.now), self.name, "leaves")

            # containers do not return a resouce at the end of a "with"
            # added a put
            #yield servers.computer.put(1)
            job_cnt -= 1
            yield env.process(return_server(env,servers.computer))


np.random.seed(0)
env = simpy.Environment()
servers  = Room(env)
env.process(Generate_Job.arriving_job(env, servers))
env.run(until = UNTIL) 
Michael
  • 1,671
  • 2
  • 4
  • 8
  • Can't thank you enough. I actually did follow your guidance in the comment in the earlier answer and got similar results, although my code is a little more messy. Also, may I know why you use add a "yield" before env.process(return_server(env,servers.computer))"? I did not add "yield" in my version and it worked too. What is the difference between the flow with a process with or without "yield"? – Matt May 31 '21 at 05:34
  • That is great you able to get some code to work. For a resource the put/release is immediate so a yield does not matter. But for containers and stores if there is not enough compacity, then a yield would wait until there was space. In this program we are managing the number of resources so the compacity is never exceeded. But still, for this program, removing the yield is better to better show we do not want to wait for a put to finish, good catch – Michael May 31 '21 at 14:02
0

So instead of interrupting the adding of a server, I just added a check after the start up delay to see if the resource is still needed. This simplifies trying to figure which server start up to cancel and avoids having to set up exception handling

I also added return_server process that only adds the server back to the resource pool if the request queue is not empty

The next enhancement would be to allow one or two servers to stay on stand by to help with average response time.

let me know if this works for you

"""
Simulation of a dynamic server pool

Server pool starts empty and servers are added as needed, but there is a delay 
simulating start up time before the server is available to fulfill a resource request
After a server is started a check is made to see if the server is still needed before
it is added to the resouce pool

Programmer: Matt
    Wrote original version

Programmer: Michael R. Gibbs
    Added server check for dynamicaly adding servers
    servers are returned to resouce pool only if needed (get queue size > 0)
"""

import simpy
import numpy as np

LAM = 8  #arival rate of jobs
MU = 2  #service rate
ALPHA = 12  #set up rate
NUM_SERVERS = 5
MAX_NUM_JOB = 10000000000
UNTIL = 10

def generate_interarrival():
  return np.random.exponential(1/LAM)

def generate_service():
  return np.random.exponential(1/MU)

def switch_on():
  return np.random.exponential(1/ALPHA)

def return_server(env,servers):
    """
    checks if the server is still needed,
    if so add back to the resource pool so waiting request can be filled
    else, do not add back to resource pool simulating shutdown
    """

    if len(servers.get_queue) > 0:
        # server is still needed
        yield servers.put(1)
        print('{0:.5f}'.format(env.now), "queuing server --")
    else:
        print('{0:.5f}'.format(env.now), "shutting down server --")

def check_servers(env, servers):
    """
    Checks the server pool to see if the pool has any avalable servers
    if not then add a server, (there will be a delay before added server becomes available)

    after the start up delay, check again to see if the server is still needed

    Call this without a yield so it does not block if a server is added
    """

    print('{0:.5f}'.format(env.now), "checking server pool", "requests:",len(servers.get_queue), "servers:", servers.level)
    
    if len(servers.get_queue) >= servers.level:
        # will need another server
        d = switch_on()
        startT = env.now + d

        print('{0:.5f}'.format(env.now), "adding a server at " + '{0:.5f}'.format(startT) + " --")
        
        # start up
        yield env.timeout(d)    #switch on time

        # check if server is still needed
        if len(servers.get_queue) > 0:
            # still need it so add
            yield servers.put(1)      
            print('{0:.5f}'.format(env.now), "added a server--")
        else:
            print('{0:.5f}'.format(env.now), "server not needed, not added--")
            
class Generate_Job(): 
    def arriving_job(env, servers):
        global  num_current_jobs, num_server_on, leaving_time_list
        for i in range(MAX_NUM_JOB):
            job = Job(name="Job%01d" % (i))
            yield env.timeout(generate_interarrival())
            print('{0:.5f}'.format(env.now), job.name, "arrives")

            env.process(job.handling(env,servers))

class Room:                             # A room containing servers (resource)
    def __init__(self, env):
        self.computer = simpy.Container(env, capacity = 10000, init = 0)


class Job(object):
    def __init__(self,name):
        self.name = name

    def handling(self, env, servers):

        # added a check to see if a resource pool needs another server.
        env.process(check_servers(env,servers.computer))
        print('{0:.5f}'.format(env.now), self.name, "requesting a server--") 

        with servers.computer.get(1) as req: 
            
            yield req 
            # if the queue is empty then the req is never filled and the next lines are never called
            # need to do this before the rescource requests
            #                       
            # yield env.timeout(switch_on())    #switch on time
            # yield servers.server.put(1)            
            print('{0:.5f}'.format(env.now), self.name, "occupies a server--")        
            yield env.timeout(generate_service())    #service time
            print('{0:.5f}'.format(env.now), self.name, "leaves")

            # containers do not return a resouce at the end of a "with"
            # added a put
            #yield servers.computer.put(1)
            yield env.process(return_server(env,servers.computer))


np.random.seed(0)
env = simpy.Environment()
servers  = Room(env)
env.process(Generate_Job.arriving_job(env, servers))
env.run(until = UNTIL) 
Michael
  • 1,671
  • 2
  • 4
  • 8
  • 1) Sorry, I made a mistake to let capacity = 10000 when create the Room class so it looks like there are unlimited number of servers, but let's say, capacity = NUM_SERVERS = 3. In this case, will the condition "if len(servers.get_queue) >= servers.level: # will need another server" remain reasonable? Since if len(servers.get_queue) returns the length of a queue including those who are waiting for servers in SETUP mode? For example, when there are 3 jobs waiting for SETUP servers, this condition may ask the process to turn on a 4th server when the 4th job arrives, while it is no available – Matt May 30 '21 at 21:31
  • 2) Maybe I got your idea of adding check after the start up delay to see if the resource is still needed. However, it seems not identical to the scenario I mentioned. It may not work when "the resource is still needed, but not needed by the customer who turned it on earlier". For example, Server 1 was requested ON when Customer 1 arrived, but then he occupies a Server 2. If Server 1 is not turned off immediately, some other customers may arrive in between and occupy Server 1 when it is turned on. – Matt May 30 '21 at 21:38
  • now you are beginning to make it complicated so container.level only shows what is in the container, not what has been created / in use. also container.capacity only limits the max in the container, not what has been created. You will need to create a server_cnt variable and manage it yourself. You can then use that variable to check if you are at your max when checking if another server is needed. – Michael May 31 '21 at 00:12
  • interrupting the start up gets complicated because the server started by a job may already finished starting up and been taken by another job. But the basics is the env.process returns a event that you can used to check if the process has finished and to interrupt the process. You will need to save each start up event to a list and then when you need to stop a start up, poll the list until you find a start up process that has not finished and meets any other selection criteria and interrupt that. It is not useful to interrupt a start up process that has already finished. – Michael May 31 '21 at 00:12