faktory 0.13.1

API bindings for the language-agnostic Faktory work server
Documentation
use std::time::Duration;
use testcontainers::core::ImageExt as _;
use testcontainers::core::IntoContainerPort as _;
use testcontainers::core::WaitFor;
use testcontainers::runners::AsyncRunner;
use testcontainers::ContainerAsync;
use testcontainers::GenericImage;

// This is the Faktory version we are testing against locally (see Makefile's
// `make faktory` phony target) and on CI (see `FAKTORY_VERSION` variable in `ent.yaml`
// workflow). Changes to the version of the Faktory server are tracked in CHANGELOG.md.
const TARGETED_FAKTORY_VERSION: &str = "1.9.1";

#[macro_export]
macro_rules! skip_check {
    () => {
        if std::env::var_os("FAKTORY_URL").is_none() {
            return;
        }
    };
}

#[macro_export]
macro_rules! skip_if_not_enterprise {
    () => {
        if std::env::var_os("FAKTORY_ENT").is_none() {
            return;
        }
    };
}

// we do not want to aggressively enforce docker engine installations,
// even though we already rely on docker when running e2e tests (see utility
// commands in Makefile), especially when testing TLS feature, where we are
// just putting a Faktory container behind an NGINX container
#[macro_export]
macro_rules! skip_if_containers_not_enabled {
    () => {
        if std::env::var_os("TESTCONTAINERS_ENABLED").is_none() {
            return;
        }
    };
}

#[macro_export]
macro_rules! assert_gt {
    ($a:expr, $b:expr $(, $rest:expr) *) => {
        assert!($a > $b $(, $rest) *)
    };
}

#[macro_export]
macro_rules! assert_gte {
    ($a:expr, $b:expr $(, $rest:expr) *) => {
        assert!($a >= $b $(, $rest) *)
    };
}

#[macro_export]
macro_rules! assert_lt {
    ($a:expr, $b:expr $(, $rest:expr) *) => {
        assert!($a < $b $(, $rest) *)
    };
}

#[macro_export]
macro_rules! assert_lte {
    ($a:expr, $b:expr $(, $rest:expr) *) => {
        assert!($a <= $b $(, $rest) *)
    };
}

#[cfg(feature = "ent")]
pub fn learn_faktory_url() -> String {
    let url = std::env::var_os("FAKTORY_URL").expect(
        "Enterprise Faktory should be running for this test, and 'FAKTORY_URL' environment variable should be provided",
    );
    url.to_str().expect("Is a utf-8 string").to_owned()
}

#[non_exhaustive]
pub struct TestContext {
    // just keep the handle so that when the TestContext
    // goes out of scope the container is stopped and removed
    _container_handle: ContainerAsync<GenericImage>,
    pub faktory_url: String,
}

/// Launch a dedicated Faktory server.
///
/// This will launch a dedicated Faktory instance in case you need one
/// for proper isolation. The Faktory address for client connections will be
/// available as [`TestContext::faktory_url`].
///
/// We've been trying to isolate tests by creating a dedicate queue for each
/// test giving that queue the test's name. This turned out to be "mostly" doing
/// the job: we still encountered weird race conditions (e.g. when testing Faktory's
/// MUTATE API), which resulted in flaky tests.
///
/// Note how this utility expects an option of container name. Most of the time.
/// we do not want the container to be left behind after the test run and so we
/// can rely on container name to be autogenerated. In some cases, though, we need
/// to be able to debug the failing test and so we want to the container to be
/// available after the test has finished. Just provide the container name (the test
/// name seems to be the most reasonable option) and the container will not be
/// automatically dropped. You can the check its address with `docker ps` and visit
/// the Faktory Web app to inspect queues and jobs.
pub async fn launch_isolated_faktory(container_name: Option<String>) -> TestContext {
    let container_request = GenericImage::new("contribsys/faktory", TARGETED_FAKTORY_VERSION)
        .with_exposed_port(7419.tcp())
        .with_exposed_port(7420.tcp())
        .with_entrypoint("/faktory")
        .with_wait_for(WaitFor::message_on_stdout("listening at :7419"))
        .with_cmd(["-b", ":7419", "-w", ":7420"])
        .with_startup_timeout(Duration::from_secs(5));
    let container_request = match container_name {
        None => container_request,
        Some(name) => container_request
            .with_reuse(testcontainers::ReuseDirective::Always)
            .with_container_name(name),
    };
    let faktory_container = container_request
        .start()
        .await
        .expect("launched container with Faktory just fine");
    let port = faktory_container
        .ports()
        .await
        .expect("post to have been published")
        .map_to_host_port_ipv4(7419)
        .expect("host post to have been assigned by OS");
    let faktory_url = format!("tcp://localhost:{}", port);
    TestContext {
        _container_handle: faktory_container,
        faktory_url,
    }
}