Skip to content

Commit

Permalink
Merge pull request fermyon#2152 from 0xE282B0/trigger-timer
Browse files Browse the repository at this point in the history
Make TimerTrigger example importable in other projects
  • Loading branch information
itowlson authored Dec 10, 2023
2 parents 0405da7 + 64cee4c commit 4847ced
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 127 deletions.
21 changes: 21 additions & 0 deletions examples/spin-timer/readme.md → examples/spin-timer/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Extending Spin with a trigger and application

## Manually
To test:

* `cargo build --release`
Expand All @@ -12,3 +13,23 @@ To test:
* `spin plugin install --file ./trigger-timer.json --yes`

Then you should be able to `spin build --up` the [guest](./app-example/).

## Pluginify

To test:

* create a `spin-pluginify.toml` file as follows:
```toml
name= "trigger-timer"
description= "Run Spin components at timed intervals"
homepage= "https://github.com/fermyon/spin/tree/main/examples/spin-timer"
version= "0.1.0"
spin_compatibility= ">=2.0"
license= "Apache-2.0"
package= "./target/release/trigger-timer"
```
* `cargo build --release`
* If pluginify plugin is not already installed run `spin plugin install pluginify`.
* `spin pluginify --install`

Then you should be able to `spin build --up` the [guest](./app-example/).
122 changes: 122 additions & 0 deletions examples/spin-timer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use std::collections::HashMap;

use clap::Args;
use serde::{Deserialize, Serialize};
use spin_app::MetadataKey;
use spin_core::async_trait;
use spin_trigger::{EitherInstance, TriggerAppEngine, TriggerExecutor};

wasmtime::component::bindgen!({
path: ".",
world: "spin-timer",
async: true
});

pub(crate) type RuntimeData = ();
pub(crate) type _Store = spin_core::Store<RuntimeData>;

#[derive(Args)]
pub struct CliArgs {
/// If true, run each component once and exit
#[clap(long)]
pub test: bool,
}

// The trigger structure with all values processed and ready
pub struct TimerTrigger {
engine: TriggerAppEngine<Self>,
speedup: u64,
component_timings: HashMap<String, u64>,
}

// Application settings (raw serialization format)
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct TriggerMetadata {
r#type: String,
speedup: Option<u64>,
}

// Per-component settings (raw serialization format)
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TimerTriggerConfig {
component: String,
interval_secs: u64,
}

const TRIGGER_METADATA_KEY: MetadataKey<TriggerMetadata> = MetadataKey::new("trigger");

#[async_trait]
impl TriggerExecutor for TimerTrigger {
const TRIGGER_TYPE: &'static str = "timer";

type RuntimeData = RuntimeData;

type TriggerConfig = TimerTriggerConfig;

type RunConfig = CliArgs;

async fn new(engine: spin_trigger::TriggerAppEngine<Self>) -> anyhow::Result<Self> {
let speedup = engine
.app()
.require_metadata(TRIGGER_METADATA_KEY)?
.speedup
.unwrap_or(1);

let component_timings = engine
.trigger_configs()
.map(|(_, config)| (config.component.clone(), config.interval_secs))
.collect();

Ok(Self {
engine,
speedup,
component_timings,
})
}

async fn run(self, config: Self::RunConfig) -> anyhow::Result<()> {
if config.test {
for component in self.component_timings.keys() {
self.handle_timer_event(component).await?;
}
} else {
// This trigger spawns threads, which Ctrl+C does not kill. So
// for this case we need to detect Ctrl+C and shut those threads
// down. For simplicity, we do this by terminating the process.
tokio::spawn(async move {
tokio::signal::ctrl_c().await.unwrap();
std::process::exit(0);
});

let speedup = self.speedup;
tokio_scoped::scope(|scope| {
// For each component, run its own timer loop
for (c, d) in &self.component_timings {
scope.spawn(async {
let duration = tokio::time::Duration::from_millis(*d * 1000 / speedup);
loop {
tokio::time::sleep(duration).await;
self.handle_timer_event(c).await.unwrap();
}
});
}
});
}
Ok(())
}
}

impl TimerTrigger {
async fn handle_timer_event(&self, component_id: &str) -> anyhow::Result<()> {
// Load the guest...
let (instance, mut store) = self.engine.prepare_instance(component_id).await?;
let EitherInstance::Component(instance) = instance else {
unreachable!()
};
let instance = SpinTimer::new(&mut store, &instance)?;
// ...and call the entry point
instance.call_handle_timer_request(&mut store).await
}
}
127 changes: 3 additions & 124 deletions examples/spin-timer/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
use std::collections::HashMap;

use anyhow::Error;
use clap::{Args, Parser};
use serde::{Deserialize, Serialize};
use spin_app::MetadataKey;
use spin_core::async_trait;
use spin_trigger::{
cli::TriggerExecutorCommand, EitherInstance, TriggerAppEngine, TriggerExecutor,
};

wasmtime::component::bindgen!({
path: ".",
world: "spin-timer",
async: true
});

pub(crate) type RuntimeData = ();
pub(crate) type _Store = spin_core::Store<RuntimeData>;
use clap::Parser;
use spin_trigger::cli::TriggerExecutorCommand;
use trigger_timer::TimerTrigger;

type Command = TriggerExecutorCommand<TimerTrigger>;

Expand All @@ -25,109 +10,3 @@ async fn main() -> Result<(), Error> {
let t = Command::parse();
t.run().await
}

#[derive(Args)]
pub struct CliArgs {
/// If true, run each component once and exit
#[clap(long)]
pub test: bool,
}

// The trigger structure with all values processed and ready
struct TimerTrigger {
engine: TriggerAppEngine<Self>,
speedup: u64,
component_timings: HashMap<String, u64>,
}

// Application settings (raw serialization format)
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct TriggerMetadata {
r#type: String,
speedup: Option<u64>,
}

// Per-component settings (raw serialization format)
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TimerTriggerConfig {
component: String,
interval_secs: u64,
}

const TRIGGER_METADATA_KEY: MetadataKey<TriggerMetadata> = MetadataKey::new("trigger");

#[async_trait]
impl TriggerExecutor for TimerTrigger {
const TRIGGER_TYPE: &'static str = "timer";

type RuntimeData = RuntimeData;

type TriggerConfig = TimerTriggerConfig;

type RunConfig = CliArgs;

async fn new(engine: spin_trigger::TriggerAppEngine<Self>) -> anyhow::Result<Self> {
let speedup = engine
.app()
.require_metadata(TRIGGER_METADATA_KEY)?
.speedup
.unwrap_or(1);

let component_timings = engine
.trigger_configs()
.map(|(_, config)| (config.component.clone(), config.interval_secs))
.collect();

Ok(Self {
engine,
speedup,
component_timings,
})
}

async fn run(self, config: Self::RunConfig) -> anyhow::Result<()> {
if config.test {
for component in self.component_timings.keys() {
self.handle_timer_event(component).await?;
}
} else {
// This trigger spawns threads, which Ctrl+C does not kill. So
// for this case we need to detect Ctrl+C and shut those threads
// down. For simplicity, we do this by terminating the process.
tokio::spawn(async move {
tokio::signal::ctrl_c().await.unwrap();
std::process::exit(0);
});

let speedup = self.speedup;
tokio_scoped::scope(|scope| {
// For each component, run its own timer loop
for (c, d) in &self.component_timings {
scope.spawn(async {
let duration = tokio::time::Duration::from_millis(*d * 1000 / speedup);
loop {
tokio::time::sleep(duration).await;
self.handle_timer_event(c).await.unwrap();
}
});
}
});
}
Ok(())
}
}

impl TimerTrigger {
async fn handle_timer_event(&self, component_id: &str) -> anyhow::Result<()> {
// Load the guest...
let (instance, mut store) = self.engine.prepare_instance(component_id).await?;
let EitherInstance::Component(instance) = instance else {
unreachable!()
};
let instance = SpinTimer::new(&mut store, &instance)?;
// ...and call the entry point
instance.call_handle_timer_request(&mut store).await
}
}
6 changes: 3 additions & 3 deletions examples/spin-timer/trigger-timer.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "trigger-timer",
"description": "IT IS I THE GREAT OZ",
"homepage": "www.example.com",
"description": "Run Spin components at timed intervals",
"homepage": "https://github.com/fermyon/spin/tree/main/examples/spin-timer",
"version": "0.1.0",
"spinCompatibility": ">=0.7",
"spinCompatibility": ">=2.0",
"license": "Apache-2.0",
"packages": [
{
Expand Down

0 comments on commit 4847ced

Please sign in to comment.