rustsingletonlazy-static

The initialization function is called twice in a `lazy_static` block


I have a big project Where I use lazy_static to create a singleton. I think there is a bug in lazy_static crate (which appears in big projects only) or I am doing something wrong because the initialization function which must be called once to create the singleton is called twice.

The structure of the project is as follows


Foo
|__foo-core
|  |__src
|  |  |__lib.rs
|  |__Cargo.toml
|
|__foo-high
|  |__src
|  |  |__lib.rs
|  |__Cargo.toml
|
|__src
|  |__lib.rs
|__Cargo.toml

Foo/foo-core/src/lib.rs

pub mod my_file {
    pub struct MyFile {
        file: std::fs::File,
    }

    impl MyFile {
        pub fn open(
            path: &'static str,
        ) -> Result<MyFile, Box<dyn std::error::Error + Send + Sync>> {
            let file_ = std::fs::File::create(path)?;
            Ok(MyFile { file: file_ })
        }
    }
}

Foo/foo-high/src/lib.rs

mod high {
    mod high_child {
        #[cfg(test)]
        mod high_child_unit_tests {
            use crate::high::my_file::*;

            #[test]
            fn some_fun_test_runner() {
                MyFile::get();
                auto_fun();
                MyFile::get();
            }

            fn auto_fun() {
                // super::super::layer ::some_fun();
                foo::high::some_fun();
            }
        }
    }

    pub mod layer {
        use crate::high::my_file::*;

        pub fn some_fun() {
            MyFile::get();
        }
    }

    mod my_file {
        pub use foo_core::my_file as core_my_file;
        use std::sync::{Mutex, MutexGuard};
        lazy_static::lazy_static! {static ref INSTANCE: Mutex<core_my_file::MyFile> = init_fun();}

        fn init_fun() -> Mutex<core_my_file::MyFile> {
            println!("INIT");
            let location = "location.txt";
            Mutex::new(core_my_file::MyFile::open(location).expect("\nSome Error has occurred."))
        }

        pub struct MyFile {}

        impl MyFile {
            pub fn get() -> MutexGuard<'static, core_my_file::MyFile> {
                println!("GET");
                INSTANCE.lock().expect("The mutex has been poisoned")
            }
        }
    }
}

pub mod layer {
    pub use crate::high::layer::*;
}

Foo/foo-core/Cargo.toml

[package]
name = "foo-core"
version = "0.1.0"
edition = "2018"

Foo/foo-high/Cargo.toml

[package]
name = "foo-high"
version = "0.1.0"
edition = "2018"

[dependencies]
foo-core = { version = "0.1.0", path = "../foo-core" }
lazy_static = "1.4.0"

[dev-dependencies]
foo = { version = "0.1.0", path = "../" }

Foo/Cargo.toml

[package]
name = "foo"
version = "0.1.0"
edition = "2018"

[dependencies]
foo-high = { version = "0.1.0", path = "foo-high" }

[workspace]
members = ["foo-high", "foo-core"]

When I ran the some_fun_test_runner test with -- --no-capture, I see 3 GETs and 2 INITs while INIT must print only once. When I changed the architecture, the function was called once but I need this architecture. And when I changed foo::high::some_fun(); (in auto_fun) to super::super::layer::some_fun();, the function was called once too. I can't understand this behavior. I used once_cell too but I have got the same results


Solution

  • You have an indirectly cyclic dependency with foo-high -> foo workspace -> foo-high again. Rust and Cargo weren't designed with dependency cycles in mind; your architecture should avoid them. To be precise, your problem arises because both instances of foo-high have their own set of statics.

    In the unit test, super::super::layer::some_fun means a fn in crate variant "A", whereas foo::high::some_fun means a fn in crate variant "B". The former works fine in your context, whereas the latter uncovers the issue with separate statics. Such duplication happens here, but also when different crate versions are pulled in somewhere down the dependency graph.

    I'm not sure about the motivation behind your crate architecture, but one potential solution is to remove the dev-dependency on foo. Then, to fake your desired module structure, you may have the following: You add at the root of foo-high

    mod foo {
        mod high {
            pub use crate::*;
        }
        pub use foo_core as core;
    }
    

    What's more, it's uncommon for a unit test to reference items by their global path. It's best to keep things local. Otherwise, it may be preferable to write an integration test instead.