Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow crate users to define and use custom models #8

Merged
merged 34 commits into from
May 16, 2021

Conversation

kirushyk
Copy link
Contributor

  • enum_dispatch approach is replaced by storing inner concrete model in Box<dyn AsModel>.
  • Model factory added to store list of model from_value constructors. Crate users can register their own custom models.
  • sim_derive crate added to provide macros for filling template methods.

@kirushyk kirushyk marked this pull request as draft April 11, 2021 22:23
@codecov-io
Copy link

Codecov Report

Merging #8 (2a9f876) into main (4135a9d) will decrease coverage by 1.36%.
The diff coverage is 51.31%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main       #8      +/-   ##
==========================================
- Coverage   76.77%   75.40%   -1.37%     
==========================================
  Files          21       23       +2     
  Lines        2058     2094      +36     
==========================================
- Hits         1580     1579       -1     
- Misses        478      515      +37     
Impacted Files Coverage Δ
src/models/exclusive_gateway.rs 70.37% <ø> (ø)
src/models/gate.rs 65.55% <ø> (ø)
src/models/generator.rs 64.63% <ø> (ø)
src/models/load_balancer.rs 72.58% <ø> (ø)
src/models/model_factory.rs 0.00% <0.00%> (ø)
src/models/parallel_gateway.rs 68.75% <ø> (ø)
src/models/processor.rs 50.67% <ø> (ø)
src/models/stochastic_gate.rs 66.66% <ø> (ø)
src/models/storage.rs 61.33% <ø> (ø)
src/models/model.rs 50.00% <7.14%> (-50.00%) ⬇️
... and 4 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4135a9d...2a9f876. Read the comment docs.

@ndebuhr
Copy link
Owner

ndebuhr commented Apr 12, 2021

This is awesome. Thanks, @kirushyk.

I'll review and test soon.

@uint, @smallstepman Any thoughts?

@uint
Copy link
Contributor

uint commented Apr 13, 2021

While the whole thing looks clever, I'm a bit wary of the overall strategy. It introduces a bit of complexity and overhead that I'm not sure you need here. This is a bit of an opinion, but I generally agree with the idea of not using trait objects for struct internals - it tends to muddy things up and eventually force people to use downcasting and boilerplate.

In a way, some of this feels like a partial reimplementation of typetag - I mean particularly the static that holds some information needed to access all the possible trait implementors, and the idea that (I think?) a user of the library would have to register() their custom models in runtime. I guess maybe that's a good thing since that could make for a smooth transition eventually? That is, if you wanted to do typetag in the future.

An alternative approach might be to make sure Simulation is generic over the AsModel trait. Something like:

pub struct Simulation<M: AsModel> {
    models: Vec<M>,
    connectors: Vec<Connector>,
    messages: Vec<Message>,
    services: Services,
}

Then a user of the library, if they really need to both add custom models and use our serialization format (this is probably fairly unlikely anyway, isn't it?), can provide their own enum that encompasses every possible model, implements AsModel, Serialize, Deserialize and there they go. I do like the idea of having a macro to make it a nicer experience.

What do you think of that, @kirushyk?

@uint
Copy link
Contributor

uint commented Apr 13, 2021

Ah, another thought: we might want to think about making (de)serialization stuff an opt-in feature so that users of the library can choose not to pull all the serde dependencies. Wouldn't this PR make that hard?

@ndebuhr
Copy link
Owner

ndebuhr commented Apr 14, 2021

Interesting.

Notes from my review and testing:

  1. It's very easy for crate users to add and use custom models (just add derive(SerializableModel) to the model struct)
  2. The metaprogramming parts of this are relatively simple and easy to understand
  3. Something like the sim_derive workspace member will probably be needed anyways, for some upcoming proc-macro-based features that I'm experimenting with
  4. lazy_static probably has a better "software supply chain health" than enum_dispatch (more contributors, longer history of usage, etc.)

These are some major benefits in my view. Especially point 1. Between (Case A) a simple implementation and complex usage vs. (Case B) complex implementation and simple usage, I think the Case B is best. Custom models will be a very common use case. Of course, simple implementation and simple usage is the best case scenario 🙂 , but this seems like a challenging problem that we may have to decide where to put the complexity.

In a way, some of this feels like a partial reimplementation of typetag - I mean particularly the static that holds some information needed to access all the possible trait implementors, and the idea that (I think?) a user of the library would have to register() their custom models in runtime. I guess maybe that's a good thing since that could make for a smooth transition eventually? That is, if you wanted to do typetag in the future.

The future compatibility with typetag is somewhat valuable, but not a big factor.

The crate is conceptually based on the Discrete Event System Specification, where models should be treated as black boxes by the simulator, and there is strict separation between the simulator and the models that it simulates. From the article you linked:

Now I’m not trying to say that using boxed trait objects is bad, there are some cases where they make a lot of sense. What I am saying is that using Box gives other developers that may consume your struct a hard time as there’s not much flexibility.

The limited flexibility may enforce the good Discrete Event System Specification pattern of "models are black boxes to the simulator" and strictly using predefined interfaces. I wouldn't say the limited flexibility is a benefit, but the Discrete Event System Specification approach makes this a smaller concern.

@uint

An alternative approach might be to make sure Simulation is generic over the AsModel trait.

I believe this would constrain a single simulation to only have models of one type? Is my understanding correct there? Almost all simulations will have a variety of model types involved, so that would be blocking concern.

Then a user of the library, if they really need to both add custom models and use our serialization format (this is probably fairly unlikely anyway, isn't it?)

I think both serialization (for NPM/JavaScript users) and custom models (for crate/Rust users) are important, but not both together.

@ndebuhr
Copy link
Owner

ndebuhr commented Apr 14, 2021

@kirushyk A side question. Can we have sim_derive as an optional feature/dependency of the sim crate, and re-export SerializableModel within sim? If so, do you think that is a good change? My hypothesis is that a single import of "sim" (with an optional derive feature) will be a better crate user experience.

fn get_type(&self) -> &'static str {
stringify!(#name)
}
fn serialize(&self) -> serde_yaml::Value {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serde_yaml::Value is in a few places in the PR diff. Does this limit serde_json serializations at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked and serde_yaml::Value works for both YAML and JSON. But serde_json::Value doesn't work out-of-box.


[dependencies]
syn = "*"
quote = "*"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't seen this dependency version format before. Is this preferred over the usual semantic versioning constraints?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I specified required versions for sync and quote dependencies.

@ndebuhr
Copy link
Owner

ndebuhr commented Apr 14, 2021

Ah, another thought: we might want to think about making (de)serialization stuff an opt-in feature so that users of the library can choose not to pull all the serde dependencies. Wouldn't this PR make that hard?

Yes, agreed. This Model trait and custom models stuff is higher priority than optional serde, but valid point. That's a downside.

@ndebuhr
Copy link
Owner

ndebuhr commented Apr 14, 2021

@kirushyk
I'm getting an error when using custom models in a WebAssembly environment, and the same error doesn't occur for other compilation targets. Is there any part of this that isn't WASM target compatible? Code below to reproduce (setup as a separate integration test file):

use std::f64::INFINITY;

use serde::{Deserialize, Serialize};
use sim::input_modeling::random_variable::ContinuousRandomVariable;
use sim::models::model_trait::{AsModel, SerializableModel};
use sim::models::{Generator, Model, ModelMessage};
use sim::simulator::{Connector, Message, Services, Simulation, WebSimulation};
use sim::utils::error::SimulationError;
use sim_derive::SerializableModel;
use wasm_bindgen_test::*;

wasm_bindgen_test_configure!(run_in_browser);

/// The passive model does nothing
#[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)]
#[serde(rename_all = "camelCase")]
pub struct Passive {
    ports_in: PortsIn,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct PortsIn {
    job: String,
}

impl Passive {
    pub fn new(job_port: String) -> Self {
        Self {
            ports_in: PortsIn { job: job_port },
        }
    }
}

impl AsModel for Passive {
    fn status(&self) -> String {
        "Passive".into()
    }

    fn events_ext(
        &mut self,
        _incoming_message: ModelMessage,
        _services: &mut Services,
    ) -> Result<Vec<ModelMessage>, SimulationError> {
        Ok(Vec::new())
    }

    fn events_int(
        &mut self,
        _services: &mut Services,
    ) -> Result<Vec<ModelMessage>, SimulationError> {
        Ok(Vec::new())
    }

    fn time_advance(&mut self, _time_delta: f64) {
        // No future events list to advance
    }

    fn until_next_event(&self) -> f64 {
        // No future events list, as a source of finite until_next_event
        // values
        INFINITY
    }
}

#[test]
fn step_n_with_custom_passive_model() {
    let models = [
        Model::new(
            String::from("generator-01"),
            Box::new(Generator::new(
                ContinuousRandomVariable::Exp { lambda: 0.5 },
                None,
                String::from("job"),
                false,
                false,
            )),
        ),
        Model::new(
            String::from("passive-01"),
            Box::new(Passive::new(String::from("job"))),
        ),
    ];
    let connectors = [Connector::new(
        String::from("connector-01"),
        String::from("generator-01"),
        String::from("passive-01"),
        String::from("job"),
        String::from("job"),
    )];
    let mut simulation = Simulation::post(models.to_vec(), connectors.to_vec());
    // 1 initialization event, and 2 events per generation
    let messages = simulation.step_n(9).unwrap();
    let generations_count = messages.len();
    let expected = 4; // 4 interarrivals from 9 steps
    assert_eq!(generations_count, expected);
}

#[wasm_bindgen_test]
fn step_n_with_custom_passive_model_wasm() {
    let models = r#"
- type: "Generator"
  id: "generator-01"
  portsIn: {}
  portsOut:
    job: "job"
  messageInterdepartureTime:
    exp:
      lambda: 0.5
- type: "Passive"
  id: "passive-01"
  portsIn:
    job: "job"
"#;
    let connectors = r#"
- id: "connector-01"
  sourceID: "generator-01"
  targetID: "passive-01"
  sourcePort: "job"
  targetPort: "job"
"#;
    let mut simulation = WebSimulation::post_yaml(&models, &connectors);
    // 1 initialization event, and 2 events per generation
    let messages: Vec<Message> = serde_json::from_str(&simulation.step_n_json(9)).unwrap();
    let generations_count = messages.len();
    let expected = 4; // 4 interarrivals from 9 steps
    assert_eq!(generations_count, expected);
}

Error from wasm-pack test --headless --chrome --firefox is:

Running headless tests in Chrome on `http://127.0.0.1:42457/`
Try find `webdriver.json` for configure browser's capabilities:
Not found
Starting new webdriver session...                 
Visiting http://127.0.0.1:36407...                
Loading page elements...                          
Waiting for test to finish...                     
                                                  
running 1 test

test custom_models::step_n_with_custom_passive_model_wasm ... FAIL

failures:

---- custom_models::step_n_with_custom_passive_model_wasm output ----
    error output:
        panicked at 'assertion failed: `(left == right)`
          left: `true`,
         right: `false`: cannot recursively acquire mutex', /rustc/2fd73fabe469357a12c2c974c140f67e7cdd76d0/library/std/src/sys/wasm/../unsupported/mutex.rs:23:9
        
        Stack:
        
        Error
            at http://127.0.0.1:36407/wasm-bindgen-test:947:19
            at http://127.0.0.1:36407/wasm-bindgen-test:154:22
            at console_error_panic_hook::Error::new::h2432f5007bbde10f (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[11082]:0x3bab9a)
            at console_error_panic_hook::hook_impl::hcf79673544a9032f (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[2165]:0x2882fa)
            at console_error_panic_hook::hook::hc388703ff51e19a8 (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[12133]:0x3c920c)
            at core::ops::function::Fn::call::h92ec3df809df7d30 (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[10044]:0x3aace9)
            at std::panicking::rust_panic_with_hook::h9d91fa0fae16500f (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[3801]:0x2fc097)
            at std::panicking::begin_panic_handler::{{closure}}::h2d82b5b2db976f3f (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[5087]:0x334e50)
            at std::sys_common::backtrace::__rust_end_short_backtrace::h24539600b7e2452c (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[12779]:0x3d11aa)
            at rust_begin_unwind (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[11548]:0x3c1614)
        
        
    
    JS exception that was thrown:
        RuntimeError: unreachable
            at __rust_start_panic (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[14206]:0x3dbdd2)
            at rust_panic (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[13129]:0x3d4ce9)
            at std::panicking::rust_panic_with_hook::h9d91fa0fae16500f (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[3801]:0x2fc0bb)
            at std::panicking::begin_panic_handler::{{closure}}::h2d82b5b2db976f3f (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[5087]:0x334e50)
            at std::sys_common::backtrace::__rust_end_short_backtrace::h24539600b7e2452c (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[12779]:0x3d11aa)
            at rust_begin_unwind (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[11548]:0x3c1614)
            at core::panicking::panic_fmt::h91d2023e5afe1929 (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[12781]:0x3d120e)
            at std::sys::wasm::mutex::Mutex::lock::hd83a394544ffda77 (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[1327]:0x21caf0)
            at std::sys_common::mutex::MovableMutex::raw_lock::h69b16960fd276720 (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[12127]:0x3c90d0)
            at std::sync::mutex::Mutex<T>::lock::h33e6c1d990be1c55 (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[11131]:0x3bb750)

failures:

    custom_models::step_n_with_custom_passive_model_wasm

test result: FAILED. 0 passed; 1 failed; 0 ignored
console.log div contained:
    panicked at 'assertion failed: `(left == right)`
      left: `true`,
     right: `false`: cannot recursively acquire mutex', /rustc/2fd73fabe469357a12c2c974c140f67e7cdd76d0/library/std/src/sys/wasm/../unsupported/mutex.rs:23:9
    
    Stack:
    
    Error
        at http://127.0.0.1:36407/wasm-bindgen-test:947:19
        at http://127.0.0.1:36407/wasm-bindgen-test:154:22
        at console_error_panic_hook::Error::new::h2432f5007bbde10f (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[11082]:0x3bab9a)
        at console_error_panic_hook::hook_impl::hcf79673544a9032f (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[2165]:0x2882fa)
        at console_error_panic_hook::hook::hc388703ff51e19a8 (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[12133]:0x3c920c)
        at core::ops::function::Fn::call::h92ec3df809df7d30 (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[10044]:0x3aace9)
        at std::panicking::rust_panic_with_hook::h9d91fa0fae16500f (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[3801]:0x2fc097)
        at std::panicking::begin_panic_handler::{{closure}}::h2d82b5b2db976f3f (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[5087]:0x334e50)
        at std::sys_common::backtrace::__rust_end_short_backtrace::h24539600b7e2452c (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[12779]:0x3d11aa)
        at rust_begin_unwind (http://127.0.0.1:36407/wasm-bindgen-test_bg.wasm:wasm-function[11548]:0x3c1614)

Error: some tests failed
error: test failed, to rerun pass '--test custom_models'
Error: Running Wasm tests with wasm-bindgen-test failed
Caused by: failed to execute `cargo test`: exited with exit code: 1
  full command: "cargo" "test" "--target" "wasm32-unknown-unknown"

@kirushyk
Copy link
Contributor Author

@ndebuhr
You need to register your Passive model first so deserialization function will be associated with "Passive" string. I added register macro to ease this operation so you can just add

register!(Passive);

to your test.

@kirushyk
Copy link
Contributor Author

I still need to fix error handling to make your test gracefully quit in case deserialization function is not found in list of known models.

@ndebuhr
Copy link
Owner

ndebuhr commented Apr 30, 2021

@kirushyk Great, thanks! WASM experimentation and testing are good now. I'll wait a couple of days to see if @uint has any additional thoughts.

I think we merge this to have a first working version. Then, iterate as opportunities arise for improvement. I'll also write some custom model tests to enable easier future iteration development.

@codecov-commenter
Copy link

codecov-commenter commented Apr 30, 2021

Codecov Report

Merging #8 (9717ebb) into main (4135a9d) will decrease coverage by 1.65%.
The diff coverage is 48.75%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main       #8      +/-   ##
==========================================
- Coverage   76.77%   75.11%   -1.66%     
==========================================
  Files          21       23       +2     
  Lines        2058     2098      +40     
==========================================
- Hits         1580     1576       -4     
- Misses        478      522      +44     
Impacted Files Coverage Δ
src/models/exclusive_gateway.rs 70.37% <ø> (ø)
src/models/gate.rs 64.44% <ø> (-1.12%) ⬇️
src/models/generator.rs 64.63% <ø> (ø)
src/models/load_balancer.rs 72.58% <ø> (ø)
src/models/model_factory.rs 0.00% <0.00%> (ø)
src/models/parallel_gateway.rs 68.75% <ø> (ø)
src/models/processor.rs 50.67% <ø> (ø)
src/models/stochastic_gate.rs 66.66% <ø> (ø)
src/models/storage.rs 61.33% <ø> (ø)
src/models/model.rs 48.14% <6.66%> (-51.86%) ⬇️
... and 7 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4135a9d...9717ebb. Read the comment docs.

@uint
Copy link
Contributor

uint commented May 1, 2021

@ndebuhr If you give me a day, I could try to produce an alternative PR, just to have a direct comparison. I really feel there is a better approach there.

@ndebuhr
Copy link
Owner

ndebuhr commented May 2, 2021

@uint Sounds great. The more options we all have to look at together, the better.

@kirushyk
Copy link
Contributor Author

I made small fix to handle the case when the model requested wasn't previously registered.

@ndebuhr ndebuhr marked this pull request as ready for review May 16, 2021 21:24
@ndebuhr ndebuhr merged commit e1bc843 into ndebuhr:main May 16, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants