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

[Feature]: Use cases and design of a shuttle-crond service #909

Closed
1 task done
morlinbrot opened this issue May 12, 2023 · 10 comments
Closed
1 task done

[Feature]: Use cases and design of a shuttle-crond service #909

morlinbrot opened this issue May 12, 2023 · 10 comments
Labels
S-Accepted This will be worked on T-Feature Request A request for a new feature

Comments

@morlinbrot
Copy link
Contributor

morlinbrot commented May 12, 2023

Describe the feature

Issue to discuss the idea of adding some form of "demon-as-a-service" cron functionality to shuttle, e.g. a shuttle-crond service or resource.

Following the discussions sparked by this first implementation which we decided to rename to request-scheduler and move to the shuttle-examples repo, thereby closing this original issue, we settled on the following for now:

Design of a crond service or resource

Any implementation of a shuttle-crond service or resource should provide a way to run any user provide logic directly (not by calling a URL). It should

  • ideally not wrap any web framework
  • should not expect the shuttle runtime to be able to provide any sort of ShuttleRuntimeContext with resources like secrets, persist, etc. This should be handled by the users themselves.

This could become an example of a community maintained extension of shuttle.

See below what a shuttle-crond could look like based on @jonaro00s and my first sketches.

Future angles to explore

During the discussion it was brought up that it might also be really useful for shuttle to provide a more generic way of running workloads.

A natural evolution that of the shuttle-crond idea would be to provide something similar like k8s' cron service spinning up pods or some other way for the user to provide binaries, containers or WASM modules via a manifest file.

@jonaro00 already started exploring the idea of being able to install native packages in the shuttle environment that the user could then access in their workloads.

I think there is a lot of design space to explore here with a number of different possible outcomes. As a starting point, I would love to hear some opinions of the shuttle team on which use cases may be worth exploring and what a possible end product should look like.

Suggestion or Example of how the feature would be used

shuttle_crond

#[async_trait]
pub trait CrontabServiceJob {
    async fn run(&mut self) -> Result<(), anyhow::Error>;

    fn schedule(&self) -> String;
}

Usage

struct MyJob {
    // User takes care of providing shuttle resources.
    secrets: Secrets,
}

#[async_trait]
impl CrontabServiceJob for MyJob {
    fn schedule(&self) -> String {
        "*/2 * * * * *".to_string()
    }

    async fn run(&mut self) -> Result<(), anyhow::Error> {
        let _secrets = self.secrets;

        // Do stuff with access to shuttle resources.
        println!("I can do anything!");

        Ok(())
    }
}

#[shuttle_runtime::main]
async fn cron(
    #[shuttle_secrets::Secrets] secrets: shuttle_secrets::SecretStore,
    // ...any other shuttle resources.
) -> ShuttleCrontab {

    let crond = ShuttleCrondService::new()
        .add_jobs([
            MyJob { secrets },
        ]);

    Ok(crond)
}

Duplicate declaration

  • I have searched the issues and this feature has not been requested before.
@morlinbrot
Copy link
Contributor Author

The above sketch was based on the example and took the form of a service. This is how it could look like as a resource that allows interacting with spun up jobs at runtime.

shuttle_crond as a resource

#[shuttle_runtime::main]
async fn axum_with_cron(
    #[shuttle_crond::Crond] crond: shuttle_crond::CrondService,
    #[shuttle_persist::Persist] persist: shuttle_persist::PersistInstance,
) -> ShuttleAxum {
    // Pass `crond` along to interact with jobs at runtime.
    let state = Arc::new(AxumServiceState { crond, persist });

    let my_axum_router = Router::new()
        .route("/start-job", get(|/* pass crond & state */| async {
            // Receive a handle to interact with the job.
            let job_id = crond.add_job(job).run();
            persist.save("job1", job_id);
        }))
        .route("/pause-job", get(|| async {
            let job_id = persist.load("job1").unwrap();
            crond.get_job(job_id).pause();
        }))
        .with_state(state);

    Ok(my_axum_router.into())
}

@iulianbarbu iulianbarbu changed the title [Feature]: Use cases and design of a shuttle-crond resource or service [Feature]: Use cases and design of a shuttle-crond service May 12, 2023
@morlinbrot
Copy link
Contributor Author

Here's a another sketch that @chesedo came up with a while back.

@morlinbrot
Copy link
Contributor Author

morlinbrot commented May 28, 2023

To keep this conversation going, I implemented a first version of the above in an external repo, https://github.com/morlinbrot/shuttle-crond, among other things to explore how community-maintained extensions to shuttle may work.

I'd like to put up two things for consideration:

On external extensions of shuttle

One thing that doesn't really yet fit with this idea is that when implementing ResourceBuilder, you always have to choose a predefined Type for it that lives in shuttle itself. For now, I can get away with just using Type::Persist as I don't really need any specific functionality. I think it would make sense to add at least one generic type that external extensions could use which don't require any further functionality (anything else couldn't be implemeneted externally anyway).

  • Should we add something like a Type::NoOp or Type::Unit Variant to common::resource::Type?

On crond functionality

My implementation is quite simple and does provide ways to interact with spawned cron jobs by returning a tokio::task::AbortHandle. I would love feedback from anybody trying this out as to what any added functionality should be.

  • Is this resource as it is enough to be added as a resource? If not, what functionality should be added?

Let me know what you think!

@jonaro00
Copy link
Member

persist.save("job1", job_id);

If jobs are taken down on restart, then there is no reason to have the ids persist across runs, so this example use case could perhaps use an in memory collection?

Otherwise the design of the resource feels mature enough to implement.

@morlinbrot
Copy link
Contributor Author

If jobs are taken down on restart, then there is no reason to have the ids persist across runs

True, that was just a quick first sketch I did. With my last comment, I actually wanted to draw attention to the actual implementation I did where I'm not persisting IDs but simply work with thread handles: https://github.com/morlinbrot/shuttle-crond

As you can see there, the lib.rs is the resource implementation and the main.rs has a short usage example.

@jonaro00
Copy link
Member

I think Job::schedule can return Schedule or Result<Schedule> so that run_job does not unwrap it. This way the user can handle schedule parsing which is good for dynamic scheduling.

Some use cases might also want a run_job_blocking (?)

The fact that the crond is moved to a tokio task in shuttle::main makes it feel like this service is not a resource, but more of a recipe 🤔

@morlinbrot
Copy link
Contributor Author

I think Job::schedule can return Schedule or Result<Schedule> so that run_job does not unwrap it. This way the user can handle schedule parsing which is good for dynamic scheduling.

This way we would create a dependency on the cron crate for the user which I would like to avoid. The way it is, we have a nice abstraction which the user doesn't have to know about and which makes usage of any specific cron crate an internal concern, enabling us to possibly changing it in the future.

Additionally, I would argue that the user-facing API is much simpler like this which I would consider to be an explicit design goal, e.g. making this resource a quick and simple "fire-and-forget" solution to scheduling some tasks.

@jonaro00
Copy link
Member

jonaro00 commented Jun 9, 2023

  1. The shuttle crond resource could reexport cron, and
  2. maybe we can combine the two alternatives with something like Into<Schedule>?

@morlinbrot
Copy link
Contributor Author

  1. The shuttle crond resource could reexport cron

Hm, yea, but that would still couple us and the user to a specific crate. I really prefer the simple option of just passing a string schedule, after all that is how you interact with cron in general (Btw, obviously the expect will be replaced with some proper error handling)

  1. maybe we can combine the two alternatives with something like Into

That could be kind of neat, I will have a look into that!

@jonaro00 jonaro00 added T-Feature Request A request for a new feature S-Accepted This will be worked on and removed A-service labels Aug 17, 2023
@jonaro00
Copy link
Member

Closing in favour of #1550

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-Accepted This will be worked on T-Feature Request A request for a new feature
Projects
None yet
Development

No branches or pull requests

3 participants