total-space 0.1.1

Explore the total space of states of communicating finite state machines.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
// 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<WorkerState, Payload> 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<WorkerState, Payload> 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<usize> {
        // 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<Payload> {
        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<Self, Payload> {
        // 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<ServerState, WorkerState, Payload> 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<usize> {
        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<Self, Payload> {
        // 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<Self, Payload> {
        let mut actions: [Option<Action<Self, Payload>>; 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<ClientState, Payload> for ClientState {
    // A client can only send one message at a time.
    fn max_in_flight_messages(&self, _instance: usize) -> Option<usize> {
        Some(1)
    }

    // What the clients do "on their own", regardless of messages.
    fn activity(&self, _instance: usize) -> Activity<Payload> {
        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<Self, Payload> {
        // 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::<WorkerState, StateId, Payload>::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::<ClientState, StateId, Payload>::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: &<SimpleModel as MetaModel>::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: &<SimpleModel as MetaModel>::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: &<SimpleModel as MetaModel>::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: &<SimpleModel as MetaModel>::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);
}