Skip to content

Quickstart

This guide walks you through creating a native mikros service from scratch: a long-running worker that starts background jobs and shuts down cleanly.

In mikros, a native service is a general-purpose app (no HTTP/gRPC surface) that initializes, starts background tasks inside start, and cleans up in stop. The trait requires start not to block; stop must finish/stop jobs.

Scaffold a bin crate

bash
cargo new quickstart-example
cd quickstart-example

Add dependencies

The example uses mikros 0.3.

toml
[dependencies]
mikros = "0.3"

Create the definitions file (service.toml)

toml
name = "quickstart-example"
types = ["native"]
version = "v0.1.0"
language = "rust"
product = "Matrix"

Implement the service

Replace src/main.rs with the following minimal:

rust
use std::sync::Arc;

use mikros::async_trait::async_trait;
use mikros::errors as merrors;
use mikros::service::builder::ServiceBuilder;
use mikros::service::context::Context;
use mikros::service::lifecycle::Lifecycle;
use mikros::service::native::NativeService;
use mikros::tokio;
use mikros::tokio::sync::{Mutex, watch};
use mikros::tokio::task::JoinHandle;

#[derive(Clone, Default)]
struct MyNative {
    shutdown_tx: Option<watch::Sender<()>>,
    shutdown_rx: Option<watch::Receiver<()>>,
    worker: Arc<Mutex<Option<JoinHandle<()>>>>,
}

#[async_trait]
impl Lifecycle for MyNative {
    // Called once when the service starts up (before/around `start`)
    async fn on_start(&mut self, _ctx: Arc<Context>) -> merrors::Result<()> {
        // Put resource initialization here if needed (DB pools, clients, etc.)
        Ok(())
    }

    // Called when the service is finishing (after `stop`)
    async fn on_finish(&self) -> merrors::Result<()> {
        // Close resources if needed
        Ok(())
    }
}

#[async_trait]
impl NativeService for MyNative {
    // IMPORTANT: start must NOT block; kick off background work and return.
    async fn start(&mut self, ctx: Arc<Context>) -> merrors::Result<()> {
        let (shutdown_tx, shutdown_rx) = watch::channel::<()>(());
        self.shutdown_tx = Some(shutdown_tx);
        self.shutdown_rx = Some(shutdown_rx);

        ctx.logger().info("native service starting");

        // Clone what the task needs
        let mut shutdown_rx = self.shutdown_rx.as_ref().unwrap().clone();
        let ctx_for_worker = ctx.clone();

        // Example background job: a simple ticker
        let handle = tokio::spawn(async move {
            ctx_for_worker.logger().info("worker loop started");

            loop {
                tokio::select! {
                    // Await a shutdown signal
                    res = shutdown_rx.changed() => {
                        match res {
                            Ok(()) => {
                                ctx_for_worker.logger().info("shutdown signal received");
                                break;
                            }
                            Err(_closed) => {
                                // Sender dropped; treat as shutdown
                                ctx_for_worker.logger().info("shutdown sender dropped");
                                break;
                            }
                        }
                    }
                    // Do periodic work
                    _ = mikros::tokio::time::sleep(std::time::Duration::from_secs(1)) => {
                        ctx_for_worker.logger().info("worker loop tick");
                    }
                }
            }

            ctx_for_worker.logger().info("worker loop finished");
        });

        // We save handle here so we can wait for it later.
        *self.worker.lock().await = Some(handle);
        Ok(())
    }

    // Called on shutdown request; must stop previously started jobs. (mikros docs)
    async fn stop(&self, ctx: Arc<Context>) {
        if let Some(tx) = &self.shutdown_tx {
            let _ = tx.send(());
        }

        // Wait for worker to finish so logs / println! flush
        if let Some(handle) = self.worker.lock().await.take()
            && let Err(join_err) = handle.await
        {
            ctx.logger()
                .error(format!("worker join error: {join_err}").as_str());
        }

        ctx.logger().info("native service stopped");
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut svc = ServiceBuilder::new()
        .native(Box::new(MyNative::default()))
        .build()?;

    Ok(svc.start().await?)
}

Run it

bash
cargo run

You’ll see JSON logs similar to mikros examples—service start, the worker loop ticking, and shutdown when you ^C.

What to customize next

  • Multiple workers: spawn more tasks in start with their own stop flags.
  • Config: read from ctx.env() or ctx.definitions() (typed config, credentials, etc.).
  • Features: if you use mikros features, wire them via ServiceBuilder::with_features(...).

Mikros is MIT/MPL-2.0 licensed