""" Classes for making complicated / interdependent sequences of FCP requests using state machine logic. Copyright (C) 2009 Darrell Karbott This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks """ # REDFLAG: move this into requestqueue? import os from fcpconnection import SUCCESS_MSGS from requestqueue import QueueableRequest # Move this to fcpconnection? def delete_client_file(client): """ Delete the file in client.inf_params.file_name. """ if client.in_params is None: return if client.in_params.file_name is None: return if not os.path.exists(client.in_params.file_name): return assert client.is_finished() os.remove(client.in_params.file_name) # allow tuples, lists? def require_state(state, state_name): """ Raise if state.name != state_name. """ if state is None or state.name != state_name: raise Exception("Illegal State") # Halting when connection drops? class StateMachine: """ CS101 state machine treatment. """ def __init__(self): # name -> State self.states = {} self.current_state = None # Subclass should set. self.transition_callback = lambda old_state, new_state: None def get_state(self, state_name): """ Get a state object by name. """ return self.states[state_name] def require_state(self, state_name): """ Assert that the current state has the name state_name """ require_state(self.current_state, state_name) def transition(self, to_name): """ Transition to the state to_name. """ new_state = self.states[to_name] assert new_state.name == to_name old_state = self.current_state old_state.leave(new_state) # Shouldn't change state. assert self.current_state == old_state self.current_state = new_state # Hmmm... order self.transition_callback(old_state, new_state) # Shouldn't change state assert self.current_state == new_state new_state.enter(old_state) # Can change state. def reset(self): """ Reset all State instances owned by the StateMachine. """ for state in self.states.values(): state.reset() class State: """ A class to represent a state in the StateMachine. """ def __init__(self, parent, name): self.parent = parent self.name = name def enter(self, from_state): """ Virtual called when the state is entered. It is legal to transition to another state here. """ pass def leave(self, to_state): """ Virtual called when the state is exited. """ # Handle canceling here. pass def reset(self): """ Pure virtual to reset the state. """ print self.name raise NotImplementedError() class StatefulRequest(QueueableRequest): """ A QueueableRequest which can be processed by a RequestQueueState. """ def __init__(self, queue): QueueableRequest.__init__(self, queue) self.tag = None # Is a delegate which can handle RequestQueue protocol but doesn't # implement it. class RequestQueueState(State): """ A State subclass which implements the RequestQueue method call protocol without subclassing it. """ def __init__(self, parent, name): State.__init__(self, parent, name) # ? -> StatefulRequest, key type is implementation dependant self.pending = {} def reset(self): """ Implementation of State virtual. """ if len(self.pending) > 0: print ("BUG?: Reseting state: %s with %i pending requests!" % (self.name, len(self.pending))) def next_runnable(self): """ Return a MinimalClient instance for the next request to be run or None if none is available. """ pass #return None # Trips pylint r201 def request_progress(self, client, msg): """ Handle non-terminal FCP messages for running requests. """ pass def request_done(self, client, msg): """ Handle terminal FCP messages for running requests. """ pass class DecisionState(RequestQueueState): """ Synthetic State which drives a transition to another state in enter().""" def __init__(self, parent, name): RequestQueueState.__init__(self, parent, name) def enter(self, from_state): """ Immediately drive transition to decide_next_state(). """ target_state = self.decide_next_state(from_state) assert target_state != self assert target_state != from_state self.parent.transition(target_state) def decide_next_state(self, dummy_from_state): """ Pure virtual. Return the state to transition into. """ print "ENOTIMPL:" + self.name return "" # Doesn't handle FCP requests. def next_runnable(self): """ Illegal. """ assert False def request_progress(self, dummy_client, dummy_msg): """ Illegal. """ assert False def request_done(self, dummy_client, dummy_msg): """ Illegal. """ assert False class RunningSingleRequest(RequestQueueState): """ RequestQueueState to run a single StatefulRequest. Caller MUST set request field. """ def __init__(self, parent, name, success_state, failure_state): RequestQueueState.__init__(self, parent, name) self.success_state = success_state self.failure_state = failure_state self.request = None self.queued = False self.final_msg = None def enter(self, dummy_from_state): """ Implementation of State virtual. """ assert not self.queued assert len(self.pending) == 0 assert not self.request is None assert not self.request.tag is None def reset(self): """ Implementation of State virtual. """ RequestQueueState.reset(self) self.request = None self.queued = False self.final_msg = None def next_runnable(self): """ Send request for the file once.""" if self.queued: return None # REDFLAG: sucky code, weird coupling self.parent.ctx.set_cancel_time(self.request) self.queued = True self.pending[self.request.tag] = self.request return self.request def request_done(self, client, msg): """ Implement virtual. """ assert self.request == client del self.pending[self.request.tag] self.final_msg = msg if msg[0] in SUCCESS_MSGS: self.parent.transition(self.success_state) return self.parent.transition(self.failure_state) class Quiescent(RequestQueueState): """ The quiescent state for the state machine. """ def __init__(self, parent, name): RequestQueueState.__init__(self, parent, name) self.prev_state = 'UNKNOWN' def enter(self, from_state): """ Implementation of State virtual. """ self.prev_state = from_state.name def reset(self): """ Implementation of State virtual. """ self.prev_state = 'UNKNOWN' RequestQueueState.reset(self) def arrived_from(self, allowed_states): """ Returns True IFF the state machine transitioned to this state from one of the states in allowed_states, False otherwise. """ return self.prev_state in allowed_states class Canceling(RequestQueueState): """ State which cancels FCP requests from the previous state and waits for them to finish. """ def __init__(self, parent, name, finished_state): RequestQueueState.__init__(self, parent, name) self.finished_state = finished_state def enter(self, from_state): """ Implementation of State virtual. """ if not hasattr(from_state, 'pending') or len(from_state.pending) == 0: self.parent.transition(self.finished_state) return self.pending = from_state.pending.copy() for request in self.pending.values(): self.parent.runner.cancel_request(request) def request_done(self, client, dummy): """ Implementation of RequestQueueState virtual. """ tag = client.tag del self.pending[tag] if len(self.pending) == 0: self.parent.transition(self.finished_state) return class CandidateRequest(StatefulRequest): """ A StatefulRequest subclass that was made from some kind of candidate. """ def __init__(self, queue): StatefulRequest.__init__(self, queue) self.candidate = None # This is not as well thought out as the other stuff in this file. # REDFLAG: better name? class RetryingRequestList(RequestQueueState): """ A RequestQueueState subclass which maintains a collection of 'candidate' objects which it uses to make request from. NOTE: The definition of what a candidate is is left to the subclass. """ def __init__(self, parent, name): RequestQueueState.__init__(self, parent, name) self.current_candidates = [] self.next_candidates = [] self.finished_candidates = [] def reset(self): """ Implementation of State virtual. """ self.current_candidates = [] self.next_candidates = [] self.finished_candidates = [] RequestQueueState.reset(self) def next_runnable(self): """ Implementation of RequestQueueState virtual. """ candidate = self.get_candidate() if candidate is None: return None request = self.make_request(candidate) self.pending[request.tag] = request return request def request_done(self, client, msg): """ Implementation of RequestQueueState virtual. """ candidate = client.candidate assert not candidate is None del self.pending[client.tag] # REDFLAG: fix signature? to get rid of candidate self.candidate_done(client, msg, candidate) ############################################################ def is_stalled(self): """ Returns True if there are no more candidates to run, False otherwise. """ return (len(self.pending) + len(self.current_candidates) + len(self.next_candidates) == 0) def pending_candidates(self): """ Returns the candiates that are currently being run by the RequestQueue. """ return [request.candidate for request in self.pending.values()] # ORDER: # 0) Candidates are popped of the lists. # 1) All candidates are popped off of current before any are popped # off of next. # 2) When current is empty AND all pending requests have finished # next and current are swapped. def get_candidate(self): """ INTERNAL: Gets the next candidate to run, or None if none is available. """ if len(self.current_candidates) == 0: if len(self.pending) != 0 or len(self.next_candidates) == 0: # i.e. Don't run requests from the next_candidates # until requests for current candidates have finished. # REDFLAG: Add a parameter to control this behavior? return None self.current_candidates = self.next_candidates self.next_candidates = [] return self.get_candidate() #print "get_candidate -- ", len(self.pending) # len(self.current_candidates), \ # len(self.next_candidates) #print "CURRENT:" #print self.current_candidates #print "NEXT:" #print self.next_candidates candidate = self.current_candidates.pop() return candidate ############################################################ def candidate_done(self, client, msg, candidate): """ Pure virtual. Add candidate to next_candidates here to retry. Add candidate to finished_candidates here if done. """ # Commented out to avoid pylint R0922 #raise NotImplementedError() pass def make_request(self, dummy_candidate): """ Subclasses must return CandidateRequest or CandidateRequest subclass for the candidate.""" #raise NotImplementedError() return CandidateRequest(self.parent)