// FILE NOT TESTED use clap::App; use clap::Arg; use clap::ArgMatches; // use core::mem::size_of; use num_traits::cast::FromPrimitive; use num_traits::cast::ToPrimitive; use std::fmt::Display; use std::fmt::Formatter; use std::fmt::Result as FormatterResult; use std::io::stdout; use std::io::BufWriter; use std::rc::Rc; use std::str; use std::str::FromStr; use strum::IntoStaticStr; use total_space::*; // The maximal number of workers we allow. const MAX_WORKERS: usize = 4; // We will have multiple clients, and multiple workers, and to send messages to them, we'll need to // know their indices in the system. This will automate placing these indices in global variables // for us. declare_agent_indices! {WORKERS} declare_agent_indices! {CLIENTS} // We will need to access the workers and clients in our conditions. This will place a reference to // the worker type in a global variable so we can do so. declare_agent_type_data! { WORKER_TYPE, WorkerState, SimpleModel } declare_agent_type_data! { CLIENT_TYPE, ClientState, SimpleModel } // We will have a single server, and to send messages to it, we'll need to know its index in the // system. This will automate placing this index in a global variable for us. declare_agent_index! {SERVER} // The payload of the messages (and activities). // // It would have been nice to have a separate type for payloads of messages exchanged between each // pair of agent types, and for each agent type activities, but doing that in Rust's type system is // extremely difficult. #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, IntoStaticStr)] enum Payload { // A client develops a need for something (activity payload). Need, // A task that a client needs the result for. // // Note that while the framework knows the source of each message, the target agent is not given // this information. If it needs to know where the message came from, then this must be // explicitly included in the message, as we do here. Task { client: usize }, // A worker completed some work (activity payload). Completed, // The result of executing the task. Result, } // The state of a worker. #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, IntoStaticStr)] enum WorkerState { // The worker is not doing anything. Idle, // The worker is working on some task. Working { client: usize }, } // The framework requires all sort of boilerplate from the enums (or structs) used for payload and // states. This macro (and the similar `impl_struct_data`) macro automates setting all this up. impl_enum_data! { // The first thing is giving the name of the enum, and its default value. The default value of // the payload has no special significance but is needed due to limitations of the framework. // Just pick any of the values. WorkerState = Self::Idle, // Displaying the data is done by first showing it using the `Debug` trait. The framework then // patches it into something like `Working(client=0)`. However, in diagrams and in general we often // want to use shorthand names. Therefore the macro supports a list of additional patches, // "from" => "to". In this example `Working(client=0)` will be converted to `WRK(C=0)`. "client" => "C", "Idle" => "IDL", "Working" => "WRK" } // Sometimes it is easier to write the code using a struct instead of an enum for the agent's state. // This may make // In such cases, you could write something like: #[cfg(struct_instead_of_enum)] #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, IntoStaticStr)] enum WorkerStateName { Idle, Working, } #[cfg(struct_instead_of_enum)] #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] struct WorkerState { // The short name of the state. name: WorkerStateName, // Additional fields. client: usize, } #[cfg(struct_instead_of_enum)] impl AgentState for WorkerState { // Arbitrary validation for the agent's state. By default this returns `None` meaning the state // is valid. Here we do so explicitly for the example's sake. If there is something wrong, the // function should return a reason string. This will be reported together with the actual state // of the agent, so there's no need to include specific details in the message itself. In theory // the validation might depend on the instance, typically it does not. // // When using a `struct` for the state, it typically allows for invalid combinations, so it is // useful to verify they never occur in practice. Using an `enum` allows for ensuring that // invalid states can't be represented in the 1st place. However, using a `struct` allows for // easier access to common fields that occur in many enum variants, and allows for `match` // statements to test such fields regardless of the value of the `name`. The best approach // "depends". fn invalid_because(&self, _instance: usize) -> Option<&'static str> { if self.name == WorkerStateName::Idle && self.client != 0 { Some("non-zero client when in the Idle state") } else { None } } } // Implement the boilerplate. #[cfg(struct_instead_of_enum)] impl_struct_data! { WorkerState = WorkerState { name: Idle, client: 0 }, // "from" => "to", ... } // Actual model logic - behavior of the worker agent. impl AgentState for WorkerState { // The maximal number of in-flight messages a worker can generate. By default this is not // restricted. In theory the maximal number of in-flight messages might depend on the instance // and the state, but typically it is a constant. fn max_in_flight_messages(&self, _instance: usize) -> Option { // In theory we might have completed a task for each client and sent the result to them, // and all of these might be in-flight at the same time. Some(agents_count!(CLIENTS)) } // What the worker does "on its own", regardless of messages. By default it is passive. We are // provided the agent instance within its type, but typically it has no effect on behavior. fn activity(&self, _instance: usize) -> Activity { match self { // The worker does nothing while listening. Self::Idle => Activity::Passive, // The worker may complete the task when working. Self::Working { .. } => Activity::Process1(Payload::Completed), } } // How the worker reacts to receiving a payload (from either an activity or from another agent). // Again we are provided the agent instance within its type, but typically it has no effect on // behavior. fn reaction(&self, _instance: usize, payload: &Payload) -> Reaction { // We have Rust's pattern matching at our disposal. Here we match on both our state (self) // and the payload we received. For complex agents, it may make more sense to match first on // one, then invoke another function that will switch on the other, or arrange the code in // any way that makes sense. Having a huge match clause that lists all options in one // unreadable mess is possible but is not readable/maintainable. match (*self, *payload) { // Accept a task while idle. (Self::Idle, Payload::Task { client }) => { // Change state to working (remember the client), not sending any messages. Reaction::Do1(Action::Change(Self::Working { client: client })) } // Completed the task we are working on. (Self::Working { client }, Payload::Completed) => { Reaction::Do1(Action::ChangeAndSend1( // Change the state back to idle. Self::Idle, // Send the result to the client. Here we see how to access the agent index of one // of several agents of the same type, using their instance. Emit::Unordered(Payload::Result, agent_index!(CLIENTS[client])), )) } // Anything else is unexpected. _ => Reaction::Unexpected, } } } // The state of a server. We'll make this a container of workers, so it doesn't have an interesting // state of its own. #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, IntoStaticStr)] enum ServerState { // The server is active (the only state). Active, } // The usual implement-boilerplate macro and patching of the display of the state. impl_enum_data! { ServerState = Self::Active, "Active" => "" // Since we only have one state there's no point in naming it in the diagrams. } // Actual model logic - behavior of the server agent. impl ContainerOf1State for ServerState { // A server can have one in-flight messages (because it only sends a single immediate message // each time). In theory the maximal number of in-flight messages might also depend on the state // of the parts, but typically it is a constant. fn max_in_flight_messages(&self, _instance: usize, _workers: &[WorkerState]) -> Option { Some(1) } // The server may defer messages if both workers are busy. fn is_deferring(&self, _instance: usize, _workers: &[WorkerState]) -> bool { true } // How the server reacts to receiving a payload (from either an activity or from another agent). // Here because the server is a container it has read-only access to the state of its worker // parts. fn reaction( &self, _instance: usize, payload: &Payload, workers: &[WorkerState], ) -> Reaction { // Match only on the payload (as we only have one state). match *payload { // A task was sent to us, presumably by a client. Payload::Task { client } => { // It is useful to factor out complex logic into functions to keep the match clauses // clean. Self::task_reaction(client, workers) } // Anything else is unexpected. _ => Reaction::Unexpected, } } } // Support functions for the server agent. impl ServerState { // We got a task from a client, return a reaction. fn task_reaction(client: usize, workers: &[WorkerState]) -> Reaction { let mut actions: [Option>; MAX_COUNT] = [None; MAX_COUNT]; let mut has_idle_worker = false; for (worker, worker_state) in workers.iter().enumerate() { match *worker_state { WorkerState::Working { client: worker_client, } => { // The worker is busy serving someone. If this someone is the client that sent // the task, we have a client that sends two tasks in parallel, which is // unexpected in this simple model. if worker_client == client { return Reaction::Unexpected; } } WorkerState::Idle => { // We found an idle worker. We can perform the action of forwarding the task to // it. This is "immediate" since we consider the worker to be a part of the // server, that is, this isn't as much a communication as it is a direct // invocation, so it can't be (meaningfully) reordered with anything else which // might be in-flight. // // In a more complex model, we'd just be exchanging messages with the worker, // and all sort of interesting race conditions would need to be dealt with - // possibly by having both a server "shadow worker" part that represents its // internal view of the worker state, and a separate independent "real worker" // agent which does the actual work. Messages to the shadow worker would be // immediate but messages to the real worker would be normal communication which // might be reordered with other messages, causing all sort of interesting // scenarios. actions[worker] = Some(Action::Send1(Emit::Immediate( Payload::Task { client }, agent_index!(WORKERS[worker]), ))); has_idle_worker = true; } } } if !has_idle_worker { // All workers are busy. In this simple model, the server simply defers receiving the // task (this is equivalent to having a queue of messages that the server reads from). return Reaction::Defer; } // We have at least one and possible several alternative workers who are idle. In this // simple model, any one of them might be used to perform the task. Reaction::Do1Of(actions) } } // The usual implement-boilerplate macro and patching of the display of the state. impl_enum_data! { Payload = Self::Result } // The state of a client. #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, IntoStaticStr)] enum ClientState { // The client is running, not bothering the server. Running, // The client is blocked, waiting for a response from the server. Blocked, } // Implement all the boilerplate for the client state. impl_enum_data! { ClientState = Self::Running, "Running" => "RUN", "Blocked" => "BLK" } // Actual model logic - behavior of the client agents. impl AgentState for ClientState { // A client can only send one message at a time. fn max_in_flight_messages(&self, _instance: usize) -> Option { Some(1) } // What the clients do "on their own", regardless of messages. fn activity(&self, _instance: usize) -> Activity { match self { // A running client may develop a need for a response from the server. This will be // delivered as a payload to the ``reaction`` function, as if it was sent from another // agent. In theory we can even use the same payloads for both messages and activities. Self::Running => Activity::Process1(Payload::Need), // A waiting client does nothing. Self::Blocked => Activity::Passive, } } // How the client reacts to receiving a payload (from either an activity or from another agent). fn reaction(&self, instance: usize, payload: &Payload) -> Reaction { // The usual match. match (self, payload) { // If we are running, and we developed a need for a response, change the state to // blocked and send a request to the server. Note we use `agent_index!` to access the // global SERVER index variable declared above. (Self::Running, Payload::Need) => Reaction::Do1(Action::ChangeAndSend1( Self::Blocked, // Here we use our instance number so the server will know how to send the response // back to us. Note we send our instance number, not our index. Emit::Unordered(Payload::Task { client: instance }, agent_index!(SERVER)), )), // If we are blocked, and got a response, go back to the running state, sending no // messages. (Self::Blocked, Payload::Result) => Reaction::Do1(Action::Change(Self::Running)), // Anything else will mark the configuration as invalid. _ => Reaction::Unexpected, } } } // We need to define the data types used in the model. // For a tiny model like this, it doesn't matter much. // However, for larger models (X * 100 * M configurations), every byte counts. // The type for identifying agent states. // // Each agent type has its own "namespace" and it is unlikely that a single agent type will have // more than 255 possible internal states (if it does, it should probably become a container of // simpler agents). index_type! { StateId, u8 } // The type for identifying messages. // // Note that this identifies both the payload, the source, the target, and any // immediate/ordering/replacement information. Here we can use 8 bits for simple models but larger // ones may need 16 bits. index_type! { MessageId, u8 } // The type for identifying invalid conditions. // // Unless one goes berserk with defining validation functions, 8 bits should be plenty. index_type! { InvalidId, u8 } // The type for identifying configurations. // // In this trivial model, 8 bits would be too much. Realistic models can easily overflow 16 bits. 32 // bits should be plenty though (you;d run out of memory way before running out of identifiers). index_type! { ConfigurationId, u32 } // The full model is parameterized by the above. // // This is fixed at compilation time and establishes limits on what the binary can do. type SimpleModel = Model< StateId, MessageId, InvalidId, ConfigurationId, Payload, 6, // MAX_AGENTS - the maximal number of agents in the model. 14, // MAX_MESSAGES - the maximal number of messages in-flight. >; // Create a model based on the command-line arguments. fn example_model(arg_matches: &ArgMatches) -> SimpleModel { let workers_arg = arg_matches.value_of("Workers").unwrap(); let workers_count = usize::from_str(workers_arg).expect("invalid number of workers"); assert!( workers_count > 0, "the number of workers must be at least 1" ); assert!( workers_count <= MAX_WORKERS, "the number of workers can be at most {}", MAX_WORKERS ); let clients_arg = arg_matches.value_of("Clients").unwrap(); let clients_count = usize::from_str(clients_arg).expect("invalid number of clients"); assert!( clients_count > 0, "the number of clients must be at least 1" ); assert!( clients_count >= workers_count, "the number of workers can be at most the number of clients" ); // Create an agent data type for the client(s). let worker_type = Rc::new(AgentTypeData::::new( "W", // The name of the agent type ("W" for short). Instances::Count(workers_count), // The number of instances from the command line. None, // No previous agent type. )); // Create an agent data type for the server. let mut server_type = Rc::new(ContainerOf1TypeData::< ServerState, WorkerState, StateId, Payload, MAX_WORKERS, >::new( "S", // The name of the agent type ("S" for short). Instances::Singleton, // Only one instance. worker_type.clone(), // The type of the agents which are parts of this container. worker_type.clone(), // Create a linked list of all the agent types. )); // By default, the order of the agents in the sequence diagram is the same as the order they are // added to the linked list above. It is possible to override this for a singleton agent, // immediately after it is created. This isn't necessary in this simple case, but for the // example's sake: Rc::get_mut(&mut server_type).unwrap().set_order(0, 10); // Create an agent data type for the client(s). let mut client_type = Rc::new(AgentTypeData::::new( "C", // The name of the agent type ("C" for short). Instances::Count(clients_count), // The number of instances from the command line. Some(server_type.clone()), // Create a linked list of all the agent types. )); // Example of overriding the order of multiple agent instances (not really needed in this // simple example). for client in 0..clients_count { Rc::get_mut(&mut client_type) .unwrap() .set_order(client, 20 + client); } // Initialize the global reference to the worker and client agent type. init_agent_type_data!(WORKER_TYPE, worker_type); init_agent_type_data!(CLIENT_TYPE, client_type); // Estimate the number of configurations, allowing override from the command line. This doesn't // have to be exact. It just pre-reserves space for this number of configurations, which make // for more efficient execution. In this case we just pre-allocate 100 states which should be // enough for reasonable model parameters. let size = model_size( arg_matches, // Allow override by command line, 100, // By default pre-allocate 100 configurations. ); // Initialize the global agent indices variables. init_agent_indices!(WORKERS, worker_type); init_agent_index!(SERVER, server_type); init_agent_indices!(CLIENTS, client_type); // Create the model, giving it the estimated size, the linked list of agent types, and a vector // of configuration validation functions (empty here). let mut model = SimpleModel::with_validator(size, client_type, invalid_because); // Add some interesting conditions. model.add_condition( "ALL_WORKERS_ARE_BUSY", all_clients_are_blocked, "All the workers are in the busy state", ); model.add_condition( "ALL_CLIENTS_ARE_BLOCKED", all_workers_are_busy, "All the clients are in the blocked blocked", ); model.add_condition( "DEFERRED_TASK", has_deferred_task, "There exists a task request message that is deferred by the server", ); // Return the result. model } // It is possible to register validation function(s) for the whole configuration. We provide an // empty such function here for the example's state; if the model doesn't have such validation(s) // then just use `Model::new` instead of `Model::with_validator`. fn invalid_because( _model: &SimpleModel, _configuration: &::Configuration, ) -> Option<&'static str> { None } // A condition on the overall configuration - in this case, that all workers are busy. This // demonstrates accessing complex data types associated with the model via the `MetaModel` trait, // and the `agent_states!` macro that iterates on the states of all agents of some type. fn all_workers_are_busy( _model: &SimpleModel, configuration: &::Configuration, ) -> bool { agent_states_iter!( configuration, WORKER_TYPE, all(|worker_state| worker_state == WorkerState::Idle) ) } // A similar condition on the clients. fn all_clients_are_blocked( _model: &SimpleModel, configuration: &::Configuration, ) -> bool { agent_states_iter!( configuration, CLIENT_TYPE, all(|client_state| client_state == ClientState::Blocked) ) } // A condition for deferred messages. This is tricky to detect because the framework doesn't track // whether messages were deferred, instead we figure that if the number of in-flight task messages // is larger than the number of idle workers, then one of these messages will need to be deferred. fn has_deferred_task( model: &SimpleModel, configuration: &::Configuration, ) -> bool { let task_messages_count = messages_iter!( model, configuration, filter(|message| matches!(message.payload, Payload::Task { .. })).count() ); let idle_workers_count = agent_states_iter!( configuration, WORKER_TYPE, filter(|worker_state| *worker_state == WorkerState::Idle).count() ); task_messages_count > idle_workers_count } // The largest data structure we keep is a Swiss hash table for the configurations. It helps // efficiency if the size of the entries of this hash table is half of a cache line or a full // cache line. The size is a linear combination of the parameters to the Model struct. The // theory is that it is better to "waste" a few bytes in the configuration to ensure that we // never need to hit two cache lines to fetch one - the larger the model, the more painful // having to fetch two cache lines instead of one becomes. assert_configuration_hash_entry_size!(SimpleModel, 32); fn main() { // Define and parse the command line options. let arg_matches = add_clap( App::new("clients-server") .arg( // Argument for number of workers. // By convention, arguments that control the model itself are in upper case. // This prevents them from being confused with generic total space arguments. Arg::with_name("Workers") .long("Workers") .short("W") .help("set the number of workers") .default_value("1"), ) .arg( // Argument for number of clients. Arg::with_name("Clients") .long("Clients") .short("C") .help("set the number of clients") .default_value("1"), ), ) .get_matches(); // Build the model using the command line options. let mut model = example_model(&arg_matches); // Buffering stdout is a really good idea, especially for large models. let mut output = BufWriter::new(stdout()); // Perform the operation(s) specified in the command line options. model.do_clap(&arg_matches, &mut output); }