Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "ee987dd3917fbde8130bd05d6649897dde87523c"
},
"path_in_vcs": ""
}
+7
View File
@@ -0,0 +1,7 @@
language: rust
rust:
- stable
script:
- cargo build
- cargo test
- cargo doc
@@ -0,0 +1,48 @@
# Contributing to TokioCronScheduler
We welcome contribution from everyone. Here are the guidelines if you are
thinking of helping us:
## Contributions
Contributions to JobScheduler should be made in the form of GitHub pull
requests. Each pull request will be reviewed and either landed in the main
tree or given feedback for changes that would be required. All contributions
should follow this format.
Should you wish to work on an issue, please claim it first by commenting on
the GitHub issue that you want to work on it. This is to prevent duplicated
efforts from contributors on the same issue.
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in JobScheduler by you, as defined in the Apache-2.0
license, shall be dual licensed as MIT/Apache-2.0, without any additional
terms or conditions.
## Pull Request Checklist
- Branch from the master branch and, if needed, rebase to the current master
branch before submitting your pull request. If it doesn't merge cleanly with
master you may be asked to rebase your changes.
- Commits should be as small as possible, while ensuring that each commit is
correct independently (i.e., each commit should compile and pass tests).
- If your patch is not getting reviewed or you need a specific person to review
it, you can @-reply a reviewer asking for a review in the pull request or a
comment.
- Add tests relevant to the fixed bug or new feature.
## Conduct
We follow the [Rust Code of Conduct](https://www.rust-lang.org/conduct.html).
For escalation or moderation issues, please contact Lori (git at loriholden dot
com) instead of the Rust moderation team.
## Communication
Beyond opening tickets on the
[job_scheduler](https://github.com/lholden/job_scheduler) project, I can be
found as `lholden` on [`irc.mozilla.org`](https://wiki.mozilla.org/IRC) and I
frequent the `#rust` channel.
File diff suppressed because it is too large Load Diff
+178
View File
@@ -0,0 +1,178 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
name = "tokio-cron-scheduler"
version = "0.15.1"
authors = ["Michael van Niekerk <mike@agri-io.co.za>"]
build = "build.rs"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Schedule tasks on tokio using cron-like annotation, at an instant or repeat them at a fixed duration. Tasks can optionally be persisted using PostgreSQL or Nats."
documentation = "https://docs.rs/tokio_cron_scheduler/"
readme = "README.md"
keywords = [
"cron",
"scheduler",
"tokio",
"nats",
"postgres",
]
categories = ["date-and-time"]
license = "MIT/Apache-2.0"
repository = "https://github.com/mvniekerk/tokio-cron-scheduler"
[features]
default = []
english = ["english-to-cron"]
has_bytes = [
"prost-build",
"prost",
]
log = [
"tracing/log",
"tracing/log-always",
]
nats_storage = [
"async-nats",
"bytes",
"has_bytes",
]
postgres_native_tls = [
"postgres_storage",
"postgres-native-tls",
]
postgres_openssl = [
"postgres_storage",
"postgres-openssl",
]
postgres_storage = [
"tokio-postgres",
"has_bytes",
]
signal = ["tokio/signal"]
[lib]
name = "tokio_cron_scheduler"
path = "src/lib.rs"
[[example]]
name = "lib"
path = "examples/lib.rs"
[[example]]
name = "nats"
path = "examples/nats_job.rs"
required-features = [
"nats_storage",
"tracing-subscriber",
]
[[example]]
name = "postgres"
path = "examples/postgres_job.rs"
required-features = [
"postgres_storage",
"tracing-subscriber",
]
[[example]]
name = "simple"
path = "examples/simple_job.rs"
required-features = ["tracing-subscriber"]
[[example]]
name = "simple-tokio-in-a-thread"
path = "examples/simple_job_tokio_in_a_thread.rs"
required-features = ["tracing-subscriber"]
[dependencies.async-nats]
version = "0.43"
features = []
optional = true
[dependencies.bytes]
version = "1"
optional = true
[dependencies.chrono]
version = "0.4"
default-features = false
[dependencies.chrono-tz]
version = "0.10"
[dependencies.croner]
version = "3.0.0"
[dependencies.english-to-cron]
version = "0.1"
optional = true
[dependencies.num-derive]
version = "0.4"
[dependencies.num-traits]
version = "0.2"
[dependencies.postgres-native-tls]
version = "0.5.0"
optional = true
[dependencies.postgres-openssl]
version = "0.5.0"
optional = true
[dependencies.prost]
version = "0.14"
optional = true
[dependencies.tokio]
version = "1"
features = [
"time",
"rt",
"sync",
]
[dependencies.tokio-postgres]
version = "0.7"
features = ["with-uuid-1"]
optional = true
[dependencies.tracing]
version = "0.1"
[dependencies.tracing-subscriber]
version = "0.3"
optional = true
[dependencies.uuid]
version = "1"
features = ["v4"]
[dev-dependencies.anyhow]
version = "1.0"
[dev-dependencies.tokio]
version = "1"
features = [
"macros",
"rt-multi-thread",
]
[build-dependencies.prost-build]
version = "0.14.1"
optional = true
+89
View File
@@ -0,0 +1,89 @@
[package]
name = "tokio-cron-scheduler"
version = "0.15.1"
authors = ["Michael van Niekerk <mike@agri-io.co.za>"]
edition = "2024"
documentation = "https://docs.rs/tokio_cron_scheduler/"
repository = "https://github.com/mvniekerk/tokio-cron-scheduler"
description = "Schedule tasks on tokio using cron-like annotation, at an instant or repeat them at a fixed duration. Tasks can optionally be persisted using PostgreSQL or Nats."
license = "MIT/Apache-2.0"
readme = "README.md"
keywords = ["cron", "scheduler", "tokio", "nats", "postgres"]
categories = ["date-and-time"]
[dependencies]
tokio = { version = "1", features = ["time", "rt", "sync"] }
croner = "3.0.0"
chrono = { version = "0.4", default-features = false }
english-to-cron = { version = "0.1", optional = true }
uuid = { version = "1", features = ["v4"] }
prost = { version = "0.14", optional = true }
tracing = "0.1"
tracing-subscriber = { version = "0.3", optional = true }
bytes = { version = "1", optional = true }
chrono-tz = { version = "0.10" }
num-traits = "0.2"
num-derive = "0.4"
[dependencies.async-nats]
version = "0.43"
features = []
optional = true
[dependencies.postgres-openssl]
version = "0.5.0"
optional = true
[dependencies.postgres-native-tls]
version = "0.5.0"
optional = true
[dependencies.tokio-postgres]
version = "0.7"
optional = true
features = ["with-uuid-1"]
[dev-dependencies]
anyhow = "1.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
[build-dependencies]
prost-build = { version = "0.14.1", optional = true }
[features]
signal = ["tokio/signal"]
has_bytes = ["prost-build", "prost"]
nats_storage = ["async-nats", "bytes", "has_bytes"]
english = ["english-to-cron"]
postgres_storage = ["tokio-postgres", "has_bytes"]
postgres_native_tls = ["postgres_storage", "postgres-native-tls"]
postgres_openssl = ["postgres_storage", "postgres-openssl"]
log = ["tracing/log", "tracing/log-always"]
default = []
[[example]]
name = "simple"
path = "examples/simple_job.rs"
required-features = ["tracing-subscriber"]
[[example]]
name = "simple-tokio-in-a-thread"
path = "examples/simple_job_tokio_in_a_thread.rs"
required-features = ["tracing-subscriber"]
[[example]]
name = "nats"
path = "examples/nats_job.rs"
required-features = ["nats_storage", "tracing-subscriber"]
[[example]]
name = "postgres"
path = "examples/postgres_job.rs"
required-features = ["postgres_storage", "tracing-subscriber"]
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Lori Holden
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2021 Michael van Niekerk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+374
View File
@@ -0,0 +1,374 @@
# tokio-cron-scheduler
Use cron-like scheduling in an async tokio environment.
Also, schedule tasks instantly or repeat them at a fixed duration.
Task data can optionally be persisted using PostgreSQL or Nats.
Inspired by https://github.com/lholden/job_scheduler
[![](https://docs.rs/tokio_cron_scheduler/badge.svg)](https://docs.rs/tokio_cron_scheduler) [![](https://img.shields.io/crates/v/tokio_cron_scheduler.svg)](https://crates.io/crates/tokio_cron_scheduler) [![](https://travis-ci.org/mvniekerk/tokio_cron_scheduler.svg?branch=master)](https://travis-ci.org/mvniekerk/tokio_cron_scheduler)
## Usage
Please see the [Documentation](https://docs.rs/tokio_cron_scheduler/) for more details.
Be sure to add the job_scheduler crate to your `Cargo.toml`:
```toml
[dependencies]
tokio-cron-scheduler = "*"
```
Creating a schedule for a job is done using any `ToString` impl, leveraging the
`Cron` type of the [croner](https://github.com/Hexagon/croner-rust) library.
The scheduling format is as follows:
```text
sec min hour day of month month day of week
* * * * * *
```
Time is specified for `UTC` and not your local timezone. Note that the year may
be omitted. If you want for your timezone, append `_tz` to the job creation calls (for instance
Job::new_async vs Job::new_async_tz).
Comma-separated values such as `5,8,10` represent more than one time value. So
for example, a schedule of `0 2,14,26 * * * *` would execute on the 2nd, 14th,
and 26th minute of every hour.
Ranges can be specified with a dash. A schedule of `0 0 * 5-10 * *` would
execute once per hour but only on days 5 through 10 of the month.
The day of the week can be specified as an abbreviation or the full name. A
schedule of `0 0 6 * * Sun,Sat` would execute at 6 am on Sunday and Saturday.
Per job, you can be notified when the jobs were started, stopped and removed. Because these notifications
are scheduled using tokio::spawn, the order of these are not guaranteed if the task finishes quickly.
A simple usage example:
```rust
use std::time::Duration;
use tokio_cron_scheduler::{Job, JobScheduler, JobSchedulerError};
#[tokio::main]
async fn main() -> Result<(), JobSchedulerError> {
let mut sched = JobScheduler::new().await?;
// Add basic cron job
sched.add(
Job::new("1/10 * * * * *", |_uuid, _l| {
println!("I run every 10 seconds");
})?
).await?;
// Add async job
sched.add(
Job::new_async("1/7 * * * * *", |uuid, mut l| {
Box::pin(async move {
println!("I run async every 7 seconds");
// Query the next execution time for this job
let next_tick = l.next_tick_for_job(uuid).await;
match next_tick {
Ok(Some(ts)) => println!("Next time for 7s job is {:?}", ts),
_ => println!("Could not get next tick for 7s job"),
}
})
})?
).await?;
// Needs the `english` feature enabled
sched.add(
Job::new_async("every 4 seconds", |uuid, mut l| {
Box::pin(async move {
println!("I run async every 4 seconds");
// Query the next execution time for this job
let next_tick = l.next_tick_for_job(uuid).await;
match next_tick {
Ok(Some(ts)) => println!("Next time for 4s job is {:?}", ts),
_ => println!("Could not get next tick for 4s job"),
}
})
})?
).await?;
// Add one-shot job with given duration
sched.add(
Job::new_one_shot(Duration::from_secs(18), |_uuid, _l| {
println!("I only run once");
})?
).await?;
// Create repeated job with given duration, make it mutable to edit it afterwards
let mut jj = Job::new_repeated(Duration::from_secs(8), |_uuid, _l| {
println!("I run repeatedly every 8 seconds");
})?;
// Add actions to be executed when the jobs starts/stop etc.
jj.on_start_notification_add(&sched, Box::new(|job_id, notification_id, type_of_notification| {
Box::pin(async move {
println!("Job {:?} was started, notification {:?} ran ({:?})", job_id, notification_id, type_of_notification);
})
})).await?;
jj.on_stop_notification_add(&sched, Box::new(|job_id, notification_id, type_of_notification| {
Box::pin(async move {
println!("Job {:?} was completed, notification {:?} ran ({:?})", job_id, notification_id, type_of_notification);
})
})).await?;
jj.on_removed_notification_add(&sched, Box::new(|job_id, notification_id, type_of_notification| {
Box::pin(async move {
println!("Job {:?} was removed, notification {:?} ran ({:?})", job_id, notification_id, type_of_notification);
})
})).await?;
sched.add(jj).await?;
// Feature 'signal' must be enabled
sched.shutdown_on_ctrl_c();
// Add code to be run during/after shutdown
sched.set_shutdown_handler(Box::new(|| {
Box::pin(async move {
println!("Shut down done");
})
}));
// Start the scheduler
sched.start().await?;
// Wait while the jobs run
tokio::time::sleep(Duration::from_secs(100)).await;
Ok(())
}
```
### Timezone changes
You can create a job using a specific timezone using the `JobBuilder` API.
chrono-tz is not included into the dependencies, so you need to add it to your Cargo.toml if you
would like to have easy creation of a `Timezone` struct.
```rust
async fn tz_job() {
let job = JobBuilder::new()
.with_timezone(chrono_tz::Africa::Johannesburg)
.with_cron_job_type()
.with_schedule("*/2 * * * *")
.unwrap()
.with_run_async(Box::new(|uuid, mut l| {
Box::pin(async move {
info!("JHB run async every 2 seconds id {:?}", uuid);
let next_tick = l.next_tick_for_job(uuid).await;
match next_tick {
Ok(Some(ts)) => info!("Next time for JHB 2s is {:?}", ts),
_ => warn!("Could not get next tick for 2s job"),
}
})
}))
.build()
.unwrap();
}
```
## Similar Libraries
* [job_scheduler](https://github.com/lholden/job_scheduler) The crate that inspired this one
* [croner-rust](https://github.com/Hexagon/croner-rust) the cron expression parser we use.
* [schedule-rs](https://github.com/mehcode/schedule-rs) is a similar rust library that implements its own cron
expression parser.
## License
TokioCronScheduler is licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
http://opensource.org/licenses/MIT)
## Custom storage
The MetadataStore and NotificationStore traits can be implemented and be used in the JobScheduler.
A default volatile hashmap-based version is provided by the SimpleMetadataStore and SimpleNotificationStore. A
persistent version using Nats is provided with NatsMetadataStore and NatsNotificationStore.
## Contributing
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.
Please see the [CONTRIBUTING](CONTRIBUTING.md) file for more information.
## Features
### english
Since 0.13.0
Enables the schedule text to be interpreted in English. This is done using
the [english-to-cron](https://crates.io/crates/english-to-cron) crate.
For instance "every 15 seconds" will be converted in the background to "0/15 * * * * ? *".
### has_bytes
Since 0.7
Enables Prost-generated data structures to be used by stores that need to get the bytes
of the data structs. The Nats and Postgres stores depend on this feature being enabled.
### postgres_storage
Since 0.6
Adds the Postgres metadata store and notification store (PostgresMetadataStore, PostgresNotificationStore). Use a
Postgres
database to store the metadata and notification data.
See [PostgreSQL docs](./postgres.md)
### postgres_native_tls
Since 0.6
Uses postgres-native-tls crate as the TLS provider for the PostgreSQL connection.
### postgres_openssl
Since 0.6
Uses the postgres-openssl crate as the TLS provider for the PostgreSQL connection.
### nats_storage
Since 0.6
Adds the Nats metadata store and notification store (NatsMetadataStore, NatsNotificationStore). Use a Nats system as a
way
to store the metadata and notifications.
See [Nats docs](./nats.md)
### signal
Since 0.5
Adds `shutdown_on_signal` and `shutdown_on_ctrl_c` to the scheduler.
Both shut the system down (stop the scheduler and remove all the tasks) when a signal
is received.
As this leverages the signal handling from Tokio, this is only available on Unix systems.
## Writing tests
When doing a tokio::test, remember to have it run in a multi-threaded context otherwise, the test
will hang on `scheduler.add()`.
For example:
```rust
#[cfg(test)]
mod test {
use tokio_cron_scheduler::{Job, JobScheduler};
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
// Needs multi_thread to test, otherwise it hangs on scheduler.add()
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
// #[tokio::test]
async fn test_schedule() {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::TRACE)
.finish();
tracing::subscriber::set_global_default(subscriber)
.expect("Setting default subscriber failed");
info!("Create scheduler");
let scheduler = JobScheduler::new().await.unwrap();
info!("Add job");
scheduler
.add(
Job::new_async("*/1 * * * * *", |_, _| {
Box::pin(async {
info!("Run every seconds");
})
})
.unwrap(),
)
.await
.expect("Should be able to add a job");
scheduler.start().await.unwrap();
tokio::time::sleep(core::time::Duration::from_secs(20)).await;
}
}
```
## Examples
### simple
Runs the in-memory hashmap-based storage
```shell
cargo run --example simple --features="tracing-subscriber"
```
### postgres
Needs a running PostgreSQL instance first:
```shell
docker run --rm -it -p 5432:5432 -e POSTGRES_USER="postgres" -e POSTGRES_PASSWORD="" -e POSTGRES_HOST_AUTH_METHOD="trust" postgres:14.1
```
Then run the example:
```shell
POSTGRES_INIT_METADATA=true POSTGRES_INIT_NOTIFICATIONS=true cargo run --example postgres --features="postgres_storage tracing-subscriber"
```
### nats
Needs a running Nats instance first with Jetstream enabled:
```shell
docker run --rm -it -p 4222:4222 -p 6222:6222 -p 7222:7222 -p 8222:8222 nats -js -DV
```
Then run the example:
```shell
cargo run --example nats --features="nats_storage tracing-subscriber"
```
## Design
### Job activity
![Job activity](./doc/job_activity.svg)
### Create job
![Create job](./doc/create_job.svg)
### Create notification
![Create notification](./doc/create_notification.svg)
### Delete job
![Delete job](./doc/delete_job.svg)
### Delete notification
![Delete notification](./doc/delete_notification.svg)
+21
View File
@@ -0,0 +1,21 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
+40
View File
@@ -0,0 +1,40 @@
use std::error::Error;
#[cfg(not(feature = "has_bytes"))]
fn no_bytes() -> Result<(), Box<dyn Error>> {
println!("No bytes");
Ok(())
}
#[cfg(feature = "has_bytes")]
fn has_bytes() -> Result<(), Box<dyn Error>> {
use std::env;
use std::fs;
use std::path::Path;
let out_dir = env::var("OUT_DIR").unwrap();
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
println!("Manifest {:}", manifest_dir);
println!("Out {:}", out_dir);
let mut prost_build = prost_build::Config::new();
prost_build.protoc_arg("--experimental_allow_proto3_optional");
prost_build.compile_protos(&["./proto/job.proto"], &["./proto/"])?;
let src = Path::new(&out_dir).join("za.co.agriio.job.rs");
let dst = Path::new(&manifest_dir).join("src/job/job_data_prost.rs");
fs::copy(&src, &dst).expect("Could not copy Protobuf file over");
println!("cargo:rerun-if-changed=proto/job.proto");
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
#[cfg(not(feature = "has_bytes"))]
no_bytes().unwrap();
#[cfg(feature = "has_bytes")]
has_bytes().unwrap();
Ok(())
}
+256
View File
@@ -0,0 +1,256 @@
= Design
== Job activity
[plantuml, job_activity_puml, svg]
....
title __Job activity__
control Timer as timer
actor Scheduler as scheduler
database MetadataStorage as storage
boundary JobActivationQueue as jobQueue
boundary NotifyQueue as notify
actor Runner as runner
entity JobCode as jobStorage
actor NotifyRunner as notifyRunner
database NotifyStorage as notifyStorage
entity NotifyCode as notifyCode
note over scheduler
Schedules tasks
end note
/ note over storage
Stores job metadata
end note
/ note over jobQueue
A queue that receives the job GUID
for jobs that needs to be run
end note
/ note over notify
A queue that receives the job GUID
and the job state it needs to be
notified of
end note
/ note over runner
Runs the job code
end note
/ note over jobStorage
Converts a job GUID to
runnable code
end note
/ note over notifyRunner
Runs notification code
end note
/ note over notifyStorage
Stores notification metadata
end note
/ note over notifyCode
Converts a notification GUID
to runnable code
end note
== Scheduling ==
timer -> scheduler : Every 1 sec
activate scheduler
scheduler -> storage : Get job guid list
storage -> scheduler
scheduler -> storage : For each guid get metadata
storage -> scheduler
scheduler -> scheduler : Filter next tick
scheduler -> storage : Next tick, last tick\nfor scheduled guids
scheduler -> jobQueue : GUIDs of next job
scheduler -> notify : GUID + SCHEDULED
deactivate scheduler
== Run the job ==
jobQueue -> runner : GUID of scheduled GUID
activate runner
runner -> jobStorage : Get runnable code\nvs GUID
jobStorage -> runner
runner -> notify : GUID + STARTED
runner -> runner : Run job
runner -> notify : GUID + DONE
deactivate runner
== Notification ==
notify -> notifyRunner : GUID + <JOB STATE>
activate notifyRunner
notifyRunner -> notifyStorage : Get notify GUIDs for\njob GUID + job state
notifyStorage -> notifyRunner
notifyRunner -> notifyCode : Get notify code to run\nusing Notify GUID
notifyCode -> notifyRunner
notifyRunner -> notifyRunner : Run notification code
deactivate notifyRunner
....
== Create job
[plantuml, job_creation_puml, svg]
....
title __Create job__
actor CreateJob as createJob
boundary JobCreateQueue as notify
boundary JobCreatedQueue as done
actor JobCreator as jobCreator
database MetadataStorage as storage
entity JobCode as runStorage
note over createJob
Starts the create job
process
end note
/ note over notify
Queue that produces
job metadata
end note
/ note over jobCreator
Handles the creation
message from the queue
end note
/ note over storage
Stores job metadata
end note
/ note over runStorage
Converts a job GUID
to runnable code
end note
/ note over done
Queue that produces
job metadata when a job
is done being made
end note
createJob -> notify : New Job
activate createJob
notify -> runStorage
notify -> jobCreator
activate jobCreator
jobCreator -> jobCreator : Generate next tick
jobCreator -> storage : Save metadata
jobCreator -> storage : Get list of GUIDs
storage -> jobCreator
jobCreator -> storage : Update list of GUIDs
jobCreator -> done : Job GUID
deactivate jobCreator
done -> createJob
deactivate createJob
....
== Create notification
[plantuml, create_notification_puml, svg]
....
title __Create notification__
actor CreateNotification as create
boundary NotifyCreateQueue as notify
boundary NotifyCreatedQueue as notified
actor NotificationCreator as creator
database NotifyStorage as notifyStorage
entity NotifyCode as notifyCode
create -> notify : Job GUID +\nNotify GUID +\nJob State
activate create
notify -> notifyCode
notify -> creator
activate creator
creator -> notifyStorage : Get notification data
notifyStorage -> creator
creator -> creator : Update job metadata\nwith new data
creator -> notifyStorage : Save
creator -> notified : Notify GUID + Job State
deactivate creator
notified -> create
deactivate create
....
== Delete notification
[plantuml, delete_notification_puml, svg]
....
title __Delete notification__
actor DeleteNotification as deleter
boundary NotifyDeleteQueue as notify
boundary NotifyDeletedQueue as notified
actor NotificationDeleter as delete
database NotifyStorage as storage
entity NotifyCode as notifyCode
deleter -> notify : Notify GUID + Job State
notify -> delete
activate delete
delete -> storage : Get notification\nmetadata
delete -> delete : Update metadata\nwith new data
delete -> storage : Save metadata
delete -> notified : Notify GUID + Job State
notified -> notifyCode
notified -> deleter
deactivate delete
....
== Delete job
[plantuml, job_deletion_puml, svg]
....
title __Delete job__
actor DeleteJob as deleter
boundary DeleteJobQueue as deleteQueue
boundary DeletedJobQueue as deletedQueue
actor Deleter as delete
database MetadataStorage as storage
entity JobCode as jobStorage
actor NotifyDeleter as notifyDelete
database NotifyStorage as notifyStorage
boundary NotifyDeletedQueue as notifyDeleted
entity NotifyCode as notifyCode
deleter -> deleteQueue : Job GUID
activate deleteQueue
deleteQueue -> delete
activate delete
group Delete Job
delete -> storage : Delete metadata
storage -> delete
delete -> deletedQueue : GUID
deletedQueue -> jobStorage
deactivate delete
deletedQueue -> deleter
end
deleteQueue -> notifyDelete
deactivate deleteQueue
group Delete notifications
activate notifyDelete
notifyDelete -> notifyStorage : Get notifications for Job ID
notifyStorage -> notifyDelete
notifyDelete -> notifyStorage : For each,\ndelete notification
notifyDelete -> notifyDeleted : For each\ndeleted notification,\nNotification GUID +\nJob GUID + State
notifyDeleted -> notifyCode
deactivate notifyDelete
end
....
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

@@ -0,0 +1,285 @@
use chrono::Utc;
use std::time::Duration;
use tokio_cron_scheduler::{Job, JobBuilder, JobScheduler, JobSchedulerError};
use tracing::{error, info, warn};
use uuid::Uuid;
pub async fn run_example(sched: &mut JobScheduler) -> Result<Vec<Uuid>, JobSchedulerError> {
#[cfg(feature = "signal")]
sched.shutdown_on_ctrl_c();
sched.set_shutdown_handler(Box::new(|| {
Box::pin(async move {
info!("Shut down done");
})
}));
let mut five_s_job = Job::new("1/5 * * * * *", |uuid, _l| {
info!(
"{:?} I run every 5 seconds id {:?}",
chrono::Utc::now(),
uuid
);
})
.unwrap();
// Adding a job notification without it being added to the scheduler will automatically add it to
// the job store, but with stopped marking
five_s_job
.on_removed_notification_add(
&sched,
Box::new(|job_id, notification_id, type_of_notification| {
Box::pin(async move {
info!(
"5s Job {:?} was removed, notification {:?} ran ({:?})",
job_id, notification_id, type_of_notification
);
})
}),
)
.await?;
let five_s_job_guid = five_s_job.guid();
sched.add(five_s_job).await?;
let mut four_s_job_async = Job::new_async_tz("1/4 * * * * *", Utc, |uuid, mut l| {
Box::pin(async move {
info!("I run async every 4 seconds id {:?}", uuid);
let next_tick = l.next_tick_for_job(uuid).await;
match next_tick {
Ok(Some(ts)) => info!("Next time for 4s is {:?}", ts),
_ => warn!("Could not get next tick for 4s job"),
}
})
})
.unwrap();
let four_s_job_async_clone = four_s_job_async.clone();
let js = sched.clone();
info!("4s job id {:?}", four_s_job_async.guid());
four_s_job_async.on_start_notification_add(&sched, Box::new(move |job_id, notification_id, type_of_notification| {
let four_s_job_async_clone = four_s_job_async_clone.clone();
let js = js.clone();
Box::pin(async move {
info!("4s Job {:?} ran on start notification {:?} ({:?})", job_id, notification_id, type_of_notification);
info!("This should only run once since we're going to remove this notification immediately.");
info!("Removed? {:?}", four_s_job_async_clone.on_start_notification_remove(&js, &notification_id).await);
})
})).await?;
four_s_job_async
.on_done_notification_add(
&sched,
Box::new(|job_id, notification_id, type_of_notification| {
Box::pin(async move {
info!(
"4s Job {:?} completed and ran notification {:?} ({:?})",
job_id, notification_id, type_of_notification
);
})
}),
)
.await?;
let four_s_job_guid = four_s_job_async.guid();
sched.add(four_s_job_async).await?;
sched
.add(
Job::new("1/30 * * * * *", |uuid, _l| {
info!("I run every 30 seconds id {:?}", uuid);
})
.unwrap(),
)
.await?;
info!(
"Sched one shot for {:?}",
chrono::Utc::now()
.checked_add_signed(chrono::Duration::seconds(10))
.unwrap()
);
sched
.add(
Job::new_one_shot(Duration::from_secs(10), |_uuid, _l| {
info!("I'm only run once");
})
.unwrap(),
)
.await?;
info!(
"Sched one shot async for {:?}",
chrono::Utc::now()
.checked_add_signed(chrono::Duration::seconds(16))
.unwrap()
);
sched
.add(
Job::new_one_shot_async(Duration::from_secs(16), |_uuid, _l| {
Box::pin(async move {
info!("I'm only run once async");
})
})
.unwrap(),
)
.await?;
let jj = Job::new_repeated(Duration::from_secs(8), |_uuid, _l| {
info!("I'm repeated every 8 seconds");
})
.unwrap();
let jj_guid = jj.guid();
sched.add(jj).await?;
let jja = Job::new_repeated_async(Duration::from_secs(7), |_uuid, _l| {
Box::pin(async move {
info!("I'm repeated async every 7 seconds");
})
})
.unwrap();
let jja_guid = jja.guid();
sched.add(jja).await?;
let utc_job = JobBuilder::new()
.with_timezone(Utc)
.with_cron_job_type()
.with_schedule("*/2 * * * * *")
.unwrap()
.with_run_async(Box::new(|uuid, mut l| {
Box::pin(async move {
info!("UTC run async every 2 seconds id {:?}", uuid);
let next_tick = l.next_tick_for_job(uuid).await;
match next_tick {
Ok(Some(ts)) => info!("Next time for UTC 2s is {:?}", ts),
_ => warn!("Could not get next tick for 2s job"),
}
})
}))
.build()
.unwrap();
let utc_job_guid = utc_job.guid();
sched.add(utc_job).await.unwrap();
let jhb_job = JobBuilder::new()
.with_timezone(chrono_tz::Africa::Johannesburg)
.with_cron_job_type()
.with_schedule("*/2 * * * * *")
.unwrap()
.with_run_async(Box::new(|uuid, mut l| {
Box::pin(async move {
info!("JHB run async every 2 seconds id {:?}", uuid);
let next_tick = l.next_tick_for_job(uuid).await;
match next_tick {
Ok(Some(ts)) => info!("Next time for JHB 2s is {:?}", ts),
_ => warn!("Could not get next tick for 2s job"),
}
})
}))
.build()
.unwrap();
let jhb_job_guid = jhb_job.guid();
sched.add(jhb_job).await.unwrap();
#[cfg(feature = "english")]
let english_job_guid = {
let english_job = JobBuilder::new()
.with_timezone(Utc)
.with_cron_job_type()
// .with_schedule("every 10 seconds")
.with_schedule("every 10 seconds")
.unwrap()
.with_run_async(Box::new(|uuid, mut l| {
Box::pin(async move {
info!("English parsed job every 10 seconds id {:?}", uuid);
let next_tick = l.next_tick_for_job(uuid).await;
match next_tick {
Ok(Some(ts)) => info!("Next time for English parsed job is is {:?}", ts),
_ => warn!("Could not get next tick for English parsed job"),
}
})
}))
.build()
.unwrap();
let english_job_guid = english_job.guid();
sched.add(english_job).await.unwrap();
english_job_guid
};
let start = sched.start().await;
if let Err(e) = start {
error!("Error starting scheduler {}", e);
return Err(e);
}
let ret = vec![
five_s_job_guid,
four_s_job_guid,
jj_guid,
jja_guid,
utc_job_guid,
jhb_job_guid,
#[cfg(feature = "english")]
english_job_guid,
];
Ok(ret)
}
pub async fn stop_example(
sched: &mut JobScheduler,
jobs: Vec<Uuid>,
) -> Result<(), JobSchedulerError> {
tokio::time::sleep(Duration::from_secs(20)).await;
for i in jobs {
sched.remove(&i).await?;
}
tokio::time::sleep(Duration::from_secs(40)).await;
info!("Goodbye.");
sched.shutdown().await?;
Ok(())
}
fn main() {
eprintln!("Should not be run on its own.");
}
#[cfg(test)]
mod test {
use tokio_cron_scheduler::{Job, JobScheduler};
use tracing::{Level, info};
use tracing_subscriber::FmtSubscriber;
// Needs multi_thread to test, otherwise it hangs on scheduler.add()
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
// #[tokio::test]
async fn test_schedule() {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::TRACE)
.finish();
tracing::subscriber::set_global_default(subscriber)
.expect("Setting default subscriber failed");
info!("Create scheduler");
let scheduler = JobScheduler::new().await.unwrap();
info!("Add job");
scheduler
.add(
Job::new_async("*/1 * * * * *", |_, _| {
Box::pin(async {
info!("Run every seconds");
})
})
.unwrap(),
)
.await
.expect("Should be able to add a job");
scheduler.start().await.unwrap();
tokio::time::sleep(core::time::Duration::from_secs(20)).await;
}
}
@@ -0,0 +1,41 @@
use crate::lib::{run_example, stop_example};
use tokio_cron_scheduler::{
JobScheduler, NatsMetadataStore, NatsNotificationStore, SimpleJobCode, SimpleNotificationCode,
};
use tracing::{Level, info};
use tracing_subscriber::FmtSubscriber;
mod lib;
#[tokio::main]
async fn main() {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::TRACE)
.finish();
tracing::subscriber::set_global_default(subscriber).expect("Setting default subscriber failed");
info!("Remember to have a running Nats instance to connect to. For example:\n");
info!("docker run --rm -it -p 4222:4222 -p 6222:6222 -p 7222:7222 -p 8222:8222 nats -js -DV");
let metadata_storage = Box::new(NatsMetadataStore::default().await);
let notification_storage = Box::new(NatsNotificationStore::default().await);
let simple_job_code = Box::new(SimpleJobCode::default());
let simple_notification_code = Box::new(SimpleNotificationCode::default());
let mut sched = JobScheduler::new_with_storage_and_code(
metadata_storage,
notification_storage,
simple_job_code,
simple_notification_code,
200,
)
.await
.unwrap();
let jobs = run_example(&mut sched)
.await
.expect("Could not run example");
stop_example(&mut sched, jobs)
.await
.expect("Could not stop example");
}
@@ -0,0 +1,52 @@
mod lib;
use crate::lib::{run_example, stop_example};
use tokio_cron_scheduler::{
JobScheduler, PostgresMetadataStore, PostgresNotificationStore, SimpleJobCode,
SimpleNotificationCode,
};
use tracing::{Level, info};
use tracing_subscriber::FmtSubscriber;
#[tokio::main]
async fn main() {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::DEBUG)
.finish();
tracing::subscriber::set_global_default(subscriber).expect("Setting default subscriber failed");
info!("Remember to have a running Postgres instance to connect to. For example:\n");
info!(
"docker run --rm -it -p 5432:5432 -e POSTGRES_USER=\"postgres\" -e POSTGRES_PASSWORD=\"\" -e POSTGRES_HOST_AUTH_METHOD=\"trust\" postgres:14.1"
);
let metadata_storage = Box::new(PostgresMetadataStore::default());
let notification_storage = Box::new(PostgresNotificationStore::default());
if std::env::var("POSTGRES_INIT_METADATA").is_err() {
info!("Set to not initialize the job metadata tables. POSTGRES_INIT_METADATA=false");
}
if std::env::var("POSTGRES_INIT_NOTIFICATIONS").is_err() {
info!(
"Set to not initialization of notification tables. POSTGRES_INIT_NOTIFICATIONS=false"
);
}
let simple_job_code = Box::new(SimpleJobCode::default());
let simple_notification_code = Box::new(SimpleNotificationCode::default());
let mut sched = JobScheduler::new_with_storage_and_code(
metadata_storage,
notification_storage,
simple_job_code,
simple_notification_code,
200,
)
.await
.unwrap();
let jobs = run_example(&mut sched)
.await
.expect("Could not run example");
stop_example(&mut sched, jobs)
.await
.expect("Could not stop example");
}
@@ -0,0 +1,22 @@
use crate::lib::{run_example, stop_example};
use tokio_cron_scheduler::JobScheduler;
use tracing::Level;
use tracing_subscriber::FmtSubscriber;
mod lib;
#[tokio::main]
async fn main() {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::TRACE)
.finish();
tracing::subscriber::set_global_default(subscriber).expect("Setting default subscriber failed");
let sched = JobScheduler::new_with_channel_size(1000).await;
let mut sched = sched.unwrap();
let jobs = run_example(&mut sched)
.await
.expect("Could not run example");
stop_example(&mut sched, jobs)
.await
.expect("Could not stop example");
}
@@ -0,0 +1,40 @@
use crate::lib::{run_example, stop_example};
use std::error::Error;
use tokio_cron_scheduler::JobScheduler;
use tracing::{Level, info};
use tracing_subscriber::FmtSubscriber;
mod lib;
fn main() {
let handle = std::thread::Builder::new()
.name("schedule thread".to_string())
.spawn(move || {
// tokio::runtime::Builder::new_current_thread() <- This hangs
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("build runtime failed")
.block_on(start())
.expect("TODO: panic message");
})
.expect("spawn thread failed");
handle.join().expect("join failed");
}
async fn start() -> Result<(), Box<dyn Error>> {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::TRACE)
.finish();
tracing::subscriber::set_global_default(subscriber).expect("Setting default subscriber failed");
info!("Creating scheduler");
let mut sched = JobScheduler::new().await?;
info!("Run example");
let jobs = run_example(&mut sched)
.await
.expect("Could not run example");
stop_example(&mut sched, jobs)
.await
.expect("Could not stop example");
Ok(())
}
+77
View File
@@ -0,0 +1,77 @@
# Migration
## 0.4, 0.5 ➡ 0.6
Architecturally 0.6 is much different from the previous versions. If you didn't implement your own scheduler, this version's only big change is the adding a reference of the scheduler when creating/removing notifications of a job.
### Removals
#### JobScheduler::new_with_scheduler()
The JobSchedulerWithoutSync trait has been removed. The new_with_scheduler method accordingly also. Replaced with new_with_storage_and_code().
#### JobStore
JobStore trait has been removed. Replaced by MetadataStore and NotificationStore traits.
#### JobSchedulerWithoutSync
No longer used.
#### SimpleJobScheduler
JobSchedulerWithoutSync trait deletion and default implementation SimpleJobScheduler removed.
#### SimpleJobStore
Removed as JobStore trait has been removed.
### API changes
#### Add &scheduler to notification add / removal
The first parameter to all of these functions on the JobLocked type needs to pass a reference to the scheduler as a first parameter.
Affected methods:
Affected | |
----------------------------------------- | -------------------------------------- |
JobLocked::on_notifications_add | |
JobLocked::on_start_notification_add | JobLocked::on_done_notification_add |
JobLocked::on_removed_notification_add | JobLocked::on_stop_notification_add |
JobLocked::on_notification_removal | |
JobLocked::on_start_notification_remove | JobLocked::on_done_notification_remove |
JobLocked::on_removed_notification_remove | JobLocked::on_stop_notification_remove |
### Additions
#### JobScheduler::new_with_storage_and_code()
Custom job metadata, job notification, job code and notification providers with the scheduler.
#### MetaDataStore
Trait needed by the scheduler to schedule jobs.
#### NotificationStore
Trait needed by the scheduler to run notifications on job start/scheduled/stop/removals.
#### ToCode
Generic trait that provides a PinnedGetFuture for a UUID.
#### JobCode
Trait that provides the runnable closures for the scheduler. Specific type of ToCode. Default implementation SimpleJobCode.
#### NotificationCode
Trait that provides the runnable notification closures for the scheduler. Specific type of ToCode. Default implementation SimpleNotificationCode.
#### SimpleMetadataStore
Default implementation for the MetadataStore.
#### SimpleNotificationStore
Default implementation for the NotificationStore.
#### PostgresMetadataStore
Postgres implementation of the MetadataStore. Needs postgres_storage feature.
#### PostgresNotificationStore
Postgres implementation of the NotificationStore. Needs postgres_storage feature.
#### NatsMetadataStore
Nats implementation of the MetadataStore. Needs nats_storage feature.
#### NatsNotificationStore
Nats implementation of the NotificationStore. Needs nats_storage feature.
+34
View File
@@ -0,0 +1,34 @@
# NATS Persistent Storage
## Setup
### NATS
You'll need a running instance of NATS that is running with Jetstream. You'll be able to run one using Docker.
From https://docs.nats.io/running-a-nats-service/introduction/running/nats_docker/jetstream_docker :
```bash
docker run --rm -it -p 4222:4222 -p 6222:6222 -p 7222:7222 -p 8222:8222 nats -js -DV
```
### Connectivity options
#### Using environmental variables
The default struct constructor for both the Metadata storage and the Notification storage uses environmental variables to set up a NatsStore.
Variable | Default | Description
---------------------- | -------------------- |-------------
NATS_HOST | nats://localhost | Nats Host to connect to
NATS_APP | Unknown Nats app | User presented name of the app connecting
NATS_USERNAME | | User name to connect with. Both this and password needs to be set otherwise it is ignored.
NATS_PASSWORD | | Password to connect with. Both this and username needs to be set otherwise it is ignored.
NATS_BUCKET_NAME | tokiocron | Key/Value bucket to store values in
NATS_BUCKET_DESCRIPTION | Tokio Cron Scheduler | key/Value bucket description.
#### Provide own Jetstream instance
Both NatsMetadataStore and NatsNotificationStore encapsulates a NatsStore that in turn encapsulates a Jetstream instance usin . Provide it accordingly. See https://github.com/nats-io/nats.rs .
+41
View File
@@ -0,0 +1,41 @@
# PostgreSQL Persistent Storage
## Setup
### PostgreSQL
You'll need a running instance of PostgreSQL. You'll be able to run one in Docker.
```shell
docker run --rm -it -p 5432:5432 -e POSTGRES_USER="postgres" -e POSTGRES_PASSWORD="" -e POSTGRES_HOST_AUTH_METHOD="trust" postgres:14.1
```
### Connectivity options
#### Using environmental variables
Variable | Default | Description
-------------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------
POSTGRES_URL | | URL as per [docs](https://docs.rs/postgres/latest/postgres/config/struct.Config.html). Other DB connection setup variables ignored if set.
POSTGRES_HOST | localhost | Host to connect to
POSTGRES_PORT | 5432 | Port to connect to
POSTGRES_DB | postgres | Database name
POSTGRES_USERNAME | postgres | Username
POSTGRES_PASSWORD | | Password
POSTGRES_APP_NAME | | Application name to register on PostgreSQL server
#### Provide own instance
Both PostgresMetadataStore and PostgresNotificationStore encapsulates a PostgresStore, which in
turn encapsulates a Tokio Postgres Client. Override accordingly.
### Other options
Environment Variable | Default | Description
------------------------------------|--------------------|-----------------------------------------------------------------------------------------------------------------------
POSTGRES_INIT_METADATA | | If set to 'true', the metadata table will be created on PostgresMetadataStore initialization.
POSTGRES_METADATA_TABLE | job | The metadata table name used by the PostgresMetadataStore.
POSTGRES_INIT_NOTIFICATIONS | | If set to 'true', the notification tables will be created on PostgresNotificationStore initizalization.
POSTGRES_NOTIFICATION_TABLE | notification | The table to hold the main notification data used by PostgresNotificationStore
POSTGRES_NOTIFICATION_STATES_TABLE | notification_state | The table to hold the states types vs notification id table. A 1:N relationship with the POSTGRES_NOTIFICATION_TABLE.
@@ -0,0 +1,86 @@
syntax = "proto3";
package za.co.agriio.job;
enum JobState {
Stop = 0;
Scheduled = 1;
Started = 2;
Done = 3;
Removed = 4;
}
enum JobType {
Cron = 0;
Repeated = 1;
OneShot = 2;
}
message CronJob {
string schedule = 1;
}
message NonCronJob {
bool repeating = 1;
uint64 repeated_every = 2;
}
message Uuid {
uint64 id1 = 1;
uint64 id2 = 2;
}
message JobStoredData {
Uuid id = 1;
optional uint64 last_updated = 2;
optional uint64 last_tick = 3;
uint64 next_tick = 4;
JobType job_type = 5;
oneof job {
CronJob cron_job = 6;
NonCronJob non_cron_job = 7;
}
uint32 count = 8;
bytes extra = 9;
bool ran = 10;
bool stopped = 11;
int32 time_offset_seconds = 12;
}
message JobIdAndNotification {
Uuid job_id = 1;
Uuid notification_id = 2;
}
message NotificationData {
JobIdAndNotification job_id = 1;
repeated JobState job_states = 2;
bytes extra = 3;
}
message NotificationIdAndState {
Uuid notification_id = 1;
JobState job_state = 2;
}
message JobAndNextTick {
Uuid id = 1;
JobType job_type = 2;
uint64 next_tick = 3;
optional uint64 last_tick = 4;
}
message ListOfUuids {
repeated Uuid uuids = 1;
}
message JobAndNotifications {
Uuid job_id = 1;
repeated Uuid notification_ids = 2;
}
message ListOfJobsAndNotifications {
repeated JobAndNotifications job_and_notifications = 1;
}
+127
View File
@@ -0,0 +1,127 @@
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::{JobState, NotificationData};
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::{JobState, NotificationData};
use crate::job::to_code::{JobCode, NotificationCode};
use crate::job::{JobToRunAsync, NotificationId};
use crate::store::{MetaDataStorage, NotificationStore};
use crate::{JobSchedulerError, JobStoredData, OnJobNotification};
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::broadcast::Sender;
use uuid::Uuid;
pub type NotificationDeletedResult =
Result<(Uuid, bool, Option<Vec<JobState>>), (JobSchedulerError, Option<NotificationId>)>;
pub struct Context {
pub job_activation_tx: Sender<Uuid>,
pub notify_tx: Sender<(Uuid, JobState)>,
pub job_create_tx: Sender<(JobStoredData, Arc<RwLock<Box<JobToRunAsync>>>)>,
pub job_created_tx: Sender<Result<Uuid, (JobSchedulerError, Option<Uuid>)>>,
pub job_delete_tx: Sender<Uuid>,
pub job_deleted_tx: Sender<Result<Uuid, (JobSchedulerError, Option<Uuid>)>>,
pub notify_create_tx: Sender<(NotificationData, Arc<RwLock<Box<OnJobNotification>>>)>,
pub notify_created_tx: Sender<Result<Uuid, (JobSchedulerError, Option<Uuid>)>>,
pub notify_delete_tx: Sender<(Uuid, Option<Vec<JobState>>)>,
pub notify_deleted_tx: Sender<NotificationDeletedResult>,
// TODO need to add when notification was deleted and there's no more references to it
pub metadata_storage: Arc<RwLock<Box<dyn MetaDataStorage + Send + Sync>>>,
pub notification_storage: Arc<RwLock<Box<dyn NotificationStore + Send + Sync>>>,
pub job_code: Arc<RwLock<Box<dyn JobCode + Send + Sync>>>,
pub notification_code: Arc<RwLock<Box<dyn NotificationCode + Send + Sync>>>,
}
impl Context {
pub fn new(
metadata_storage: Arc<RwLock<Box<dyn MetaDataStorage + Send + Sync>>>,
notification_storage: Arc<RwLock<Box<dyn NotificationStore + Send + Sync>>>,
job_code: Arc<RwLock<Box<dyn JobCode + Send + Sync>>>,
notification_code: Arc<RwLock<Box<dyn NotificationCode + Send + Sync>>>,
) -> Self {
let (job_activation_tx, _job_activation_rx) = tokio::sync::broadcast::channel(200);
let (notify_tx, _notify_rx) = tokio::sync::broadcast::channel(200);
let (job_create_tx, _job_create_rx) = tokio::sync::broadcast::channel(200);
let (job_created_tx, _job_created_rx) = tokio::sync::broadcast::channel(200);
let (job_delete_tx, _job_delete_rx) = tokio::sync::broadcast::channel(200);
let (job_deleted_tx, _job_deleted_rx) = tokio::sync::broadcast::channel(200);
let (notify_create_tx, _notify_create_rx) = tokio::sync::broadcast::channel(200);
let (notify_created_tx, _notify_created_rx) = tokio::sync::broadcast::channel(200);
let (notify_delete_tx, _notify_delete_rx) = tokio::sync::broadcast::channel(200);
let (notify_deleted_tx, _notify_deleted_rx) = tokio::sync::broadcast::channel(200);
Self {
job_activation_tx,
notify_tx,
job_create_tx,
job_created_tx,
job_delete_tx,
job_deleted_tx,
notify_create_tx,
notify_created_tx,
notify_delete_tx,
notify_deleted_tx,
metadata_storage,
notification_storage,
job_code,
notification_code,
}
}
pub fn new_with_channel_size(
metadata_storage: Arc<RwLock<Box<dyn MetaDataStorage + Send + Sync>>>,
notification_storage: Arc<RwLock<Box<dyn NotificationStore + Send + Sync>>>,
job_code: Arc<RwLock<Box<dyn JobCode + Send + Sync>>>,
notification_code: Arc<RwLock<Box<dyn NotificationCode + Send + Sync>>>,
channel_size: usize,
) -> Self {
let (job_activation_tx, _job_activation_rx) = tokio::sync::broadcast::channel(channel_size);
let (notify_tx, _notify_rx) = tokio::sync::broadcast::channel(channel_size);
let (job_create_tx, _job_create_rx) = tokio::sync::broadcast::channel(channel_size);
let (job_created_tx, _job_created_rx) = tokio::sync::broadcast::channel(channel_size);
let (job_delete_tx, _job_delete_rx) = tokio::sync::broadcast::channel(channel_size);
let (job_deleted_tx, _job_deleted_rx) = tokio::sync::broadcast::channel(channel_size);
let (notify_create_tx, _notify_create_rx) = tokio::sync::broadcast::channel(channel_size);
let (notify_created_tx, _notify_created_rx) = tokio::sync::broadcast::channel(channel_size);
let (notify_delete_tx, _notify_delete_rx) = tokio::sync::broadcast::channel(channel_size);
let (notify_deleted_tx, _notify_deleted_rx) = tokio::sync::broadcast::channel(channel_size);
Self {
job_activation_tx,
notify_tx,
job_create_tx,
job_created_tx,
job_delete_tx,
job_deleted_tx,
notify_create_tx,
notify_created_tx,
notify_delete_tx,
notify_deleted_tx,
metadata_storage,
notification_storage,
job_code,
notification_code,
}
}
}
impl Clone for Context {
fn clone(&self) -> Self {
Self {
job_activation_tx: self.job_activation_tx.clone(),
notify_tx: self.notify_tx.clone(),
job_create_tx: self.job_create_tx.clone(),
job_created_tx: self.job_created_tx.clone(),
job_delete_tx: self.job_delete_tx.clone(),
job_deleted_tx: self.job_deleted_tx.clone(),
notify_create_tx: self.notify_create_tx.clone(),
notify_created_tx: self.notify_created_tx.clone(),
notify_delete_tx: self.notify_delete_tx.clone(),
notify_deleted_tx: self.notify_deleted_tx.clone(),
metadata_storage: self.metadata_storage.clone(),
notification_storage: self.notification_storage.clone(),
job_code: self.job_code.clone(),
notification_code: self.notification_code.clone(),
}
}
}
+47
View File
@@ -0,0 +1,47 @@
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
#[derive(Debug, Clone)]
pub enum JobSchedulerError {
CantRemove,
CantAdd,
CantInit,
TickError,
CantGetTimeUntil,
Shutdown,
ShutdownNotifier,
AddShutdownNotifier,
RemoveShutdownNotifier,
FetchJob,
SaveJob,
StartScheduler,
ErrorLoadingGuidList,
ErrorLoadingJob,
CouldNotGetTimeUntilNextTick,
GetJobData,
GetJobStore,
JobTick,
UpdateJobData,
NoNextTick,
CantListGuids,
CantListNextTicks,
NotifyOnStateError,
ParseSchedule,
JobTypeNotSet,
RunOrRunAsyncNotSet,
ScheduleNotSet,
#[cfg(feature = "nats_storage")]
BuilderNeedsField(String),
#[cfg(feature = "nats_storage")]
NatsCouldNotConnect(String),
#[cfg(feature = "nats_storage")]
NatsCouldNotCreateKvStore(String),
}
impl Display for JobSchedulerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self, f)
}
}
impl Error for JobSchedulerError {}
@@ -0,0 +1,256 @@
use crate::job::cron_job::CronJob;
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data;
#[cfg(not(feature = "has_bytes"))]
pub use crate::job::job_data::{JobStoredData, JobType, Uuid};
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost;
#[cfg(feature = "has_bytes")]
pub use crate::job::job_data_prost::{JobStoredData, JobType, Uuid};
use crate::job::{JobLocked, nop, nop_async};
use crate::{JobSchedulerError, JobToRun, JobToRunAsync};
use chrono::{Offset, TimeZone, Utc};
use core::time::Duration;
use croner::Cron;
use croner::parser::CronParser;
use std::sync::{Arc, RwLock};
use std::time::Instant;
use uuid::Uuid as UuidUuid;
pub struct JobBuilder<T> {
pub job_id: Option<Uuid>,
pub timezone: Option<T>,
pub job_type: Option<JobType>,
pub schedule: Option<Cron>,
pub run: Option<Box<JobToRun>>,
pub run_async: Option<Box<JobToRunAsync>>,
pub duration: Option<Duration>,
pub repeating: Option<bool>,
pub instant: Option<Instant>,
}
impl JobBuilder<Utc> {
pub fn new() -> Self {
Self {
job_id: None,
timezone: None,
job_type: None,
schedule: None,
run: None,
run_async: None,
duration: None,
repeating: None,
instant: None,
}
}
}
impl<T: TimeZone> JobBuilder<T> {
pub fn with_timezone<U: TimeZone>(self, timezone: U) -> JobBuilder<U> {
JobBuilder {
timezone: Some(timezone),
job_id: self.job_id,
job_type: self.job_type,
schedule: self.schedule,
run: self.run,
run_async: self.run_async,
duration: self.duration,
repeating: self.repeating,
instant: self.instant,
}
}
pub fn with_job_id(self, job_id: Uuid) -> Self {
Self {
job_id: Some(job_id),
..self
}
}
pub fn with_job_type(self, job_type: JobType) -> Self {
Self {
job_type: Some(job_type),
..self
}
}
pub fn with_cron_job_type(self) -> Self {
Self {
job_type: Some(JobType::Cron),
..self
}
}
pub fn with_repeated_job_type(self) -> Self {
Self {
job_type: Some(JobType::Repeated),
..self
}
}
pub fn with_one_shot_job_type(self) -> Self {
Self {
job_type: Some(JobType::OneShot),
..self
}
}
pub fn with_schedule<TS>(self, schedule: TS) -> Result<Self, JobSchedulerError>
where
TS: ToString,
{
let schedule = JobLocked::schedule_to_cron(schedule)?;
let schedule = CronParser::builder()
.seconds(croner::parser::Seconds::Required)
.build()
.parse(&schedule)
.map_err(|_| JobSchedulerError::ParseSchedule)?;
Ok(Self {
schedule: Some(schedule),
..self
})
}
pub fn with_run_sync(self, job: Box<JobToRun>) -> Self {
Self {
run: Some(Box::new(job)),
..self
}
}
pub fn with_run_async(self, job: Box<JobToRunAsync>) -> Self {
Self {
run_async: Some(Box::new(job)),
..self
}
}
pub fn every_seconds(self, seconds: u64) -> Self {
Self {
duration: Some(Duration::from_secs(seconds)),
repeating: Some(true),
..self
}
}
pub fn after_seconds(self, seconds: u64) -> Self {
Self {
duration: Some(Duration::from_secs(seconds)),
repeating: Some(false),
..self
}
}
pub fn at_instant(self, instant: Instant) -> Self {
Self {
instant: Some(instant),
..self
}
}
pub fn build(self) -> Result<JobLocked, JobSchedulerError> {
if self.job_type.is_none() {
return Err(JobSchedulerError::JobTypeNotSet);
}
let job_type = self.job_type.unwrap();
let (run, run_async) = (self.run, self.run_async);
if run.is_none() && run_async.is_none() {
return Err(JobSchedulerError::RunOrRunAsyncNotSet);
}
let async_job = run_async.is_some();
match job_type {
JobType::Cron => {
if self.schedule.is_none() {
return Err(JobSchedulerError::ScheduleNotSet);
}
let schedule = self.schedule.unwrap();
let time_offset_seconds = if let Some(tz) = self.timezone.as_ref() {
tz.offset_from_utc_datetime(&Utc::now().naive_local())
.fix()
.local_minus_utc()
} else {
0
};
Ok(JobLocked(Arc::new(RwLock::new(Box::new(CronJob {
data: JobStoredData {
id: self.job_id.or(Some(UuidUuid::new_v4().into())),
last_updated: None,
last_tick: None,
next_tick: match &self.timezone {
Some(timezone) => schedule
.find_next_occurrence(&Utc::now().with_timezone(timezone), false)
.map(|tz_time| tz_time.timestamp() as u64)
.unwrap_or(0),
None => schedule
.find_next_occurrence(&Utc::now(), false)
.map(|t| t.timestamp() as u64)
.unwrap_or(0),
},
job_type: JobType::Cron.into(),
count: 0,
extra: vec![],
ran: false,
stopped: false,
#[cfg(feature = "has_bytes")]
job: Some(job_data_prost::job_stored_data::Job::CronJob(
job_data_prost::CronJob {
schedule: schedule.pattern.to_string(),
},
)),
#[cfg(not(feature = "has_bytes"))]
job: Some(job_data::job_stored_data::Job::CronJob(job_data::CronJob {
schedule: schedule.pattern.to_string(),
})),
time_offset_seconds,
},
run: run.unwrap_or(Box::new(nop)),
run_async: run_async.unwrap_or(Box::new(nop_async)),
async_job,
})))))
}
JobType::Repeated => Err(JobSchedulerError::NoNextTick),
JobType::OneShot => Err(JobSchedulerError::NoNextTick),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::JobScheduler;
use chrono::Timelike;
#[tokio::test]
async fn test_timezone_job_builder() {
let mut scheduler = JobScheduler::new().await.unwrap();
let job_id = scheduler
.add(
JobBuilder::new()
.with_timezone(chrono_tz::Europe::Paris)
.with_cron_job_type()
.with_schedule("0 30 9 * * *")
.unwrap()
.with_run_async(Box::new(|_uuid, _lock| Box::pin(async move {})))
.build()
.unwrap(),
)
.await
.unwrap();
let next_tick = scheduler
.next_tick_for_job(job_id)
.await
.unwrap()
.expect("Should have next_tick");
let paris_time = next_tick.with_timezone(&chrono_tz::Europe::Paris);
assert_eq!(paris_time.hour(), 9);
assert_eq!(paris_time.minute(), 30);
}
}
@@ -0,0 +1,131 @@
use crate::context::Context;
use crate::job::{JobLocked, JobToRunAsync};
use crate::store::MetaDataStorage;
use crate::{JobSchedulerError, JobStoredData};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::broadcast::{Receiver, Sender};
use tracing::error;
use uuid::Uuid;
#[derive(Default)]
pub struct JobCreator {}
impl JobCreator {
async fn listen_to_additions(
storage: Arc<RwLock<Box<dyn MetaDataStorage + Send + Sync>>>,
mut rx: Receiver<(JobStoredData, Arc<RwLock<Box<JobToRunAsync>>>)>,
tx_created: Sender<Result<Uuid, (JobSchedulerError, Option<Uuid>)>>,
) {
loop {
let val = rx.recv().await;
if let Err(e) = val {
error!("Error receiving {:?}", e);
break;
}
let (data, _) = val.unwrap();
let uuid: Uuid = match data.id.as_ref().map(|b| b.into()) {
Some(uuid) => uuid,
None => {
if let Err(e) = tx_created.send(Err((JobSchedulerError::CantAdd, None))) {
error!("Error sending creation error {:?}", e);
}
continue;
}
};
{
let mut storage = storage.write().await;
let saved = storage.add_or_update(data).await;
if let Err(e) = saved {
error!("Error saving job metadata {:?}", e);
if let Err(e) = tx_created.send(Err((e, Some(uuid)))) {
error!("Could not send failure {:?}", e);
}
continue;
}
}
if let Err(e) = tx_created.send(Ok(uuid)) {
error!("Error sending created job {:?}", e);
}
}
}
pub fn init(
&self,
context: &Context,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let rx = context.job_create_tx.subscribe();
let tx_created = context.job_created_tx.clone();
let storage = context.metadata_storage.clone();
Box::pin(async move {
tokio::spawn(JobCreator::listen_to_additions(storage, rx, tx_created));
Ok(())
})
}
pub async fn add(context: &Context, mut job: JobLocked) -> Result<Uuid, JobSchedulerError> {
let tx = context.job_create_tx.clone();
let mut rx = context.job_created_tx.subscribe();
let data = job.job_data();
let uuid = job.guid();
if let Err(e) = data {
error!("Error getting job data {e:?}");
return Err(e);
}
let data = data.unwrap();
let job: Box<JobToRunAsync> = Box::new(move |job_id, job_scheduler| {
let job = job.clone();
Box::pin(async move {
let job_done = {
let w = job.0.write();
if let Err(e) = w {
error!("Error getting job {:?}", e);
return;
}
let mut w = w.unwrap();
w.run(job_scheduler)
};
let job_done = job_done.await;
match job_done {
Err(e) => {
error!("Error running job {:?} {:?}", job_id, e);
}
Ok(val) => {
if !val {
error!("Error running job {:?}", job_id);
}
}
}
})
});
let job = Arc::new(RwLock::new(job));
if let Err(_e) = tx.send((data, job)) {
error!("Error sending new job");
return Err(JobSchedulerError::CantAdd);
}
while let Ok(val) = rx.recv().await {
match val {
Ok(ret_uuid) => {
if ret_uuid == uuid {
return Ok(uuid);
}
}
Err((e, Some(ret_uuid))) => {
if ret_uuid == uuid {
return Err(e);
}
}
_ => {}
}
}
Err(JobSchedulerError::CantAdd)
}
}
@@ -0,0 +1,126 @@
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::{JobStoredData, JobType};
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::{JobStoredData, JobType};
use crate::job::{Job, JobToRunAsync};
use crate::{JobScheduler, JobSchedulerError, JobToRun};
use chrono::{DateTime, Utc};
use croner::Cron;
use tokio::sync::oneshot::Receiver;
use tracing::error;
use uuid::Uuid;
pub struct CronJob {
pub data: JobStoredData,
pub run: Box<JobToRun>,
pub run_async: Box<JobToRunAsync>,
pub async_job: bool,
}
impl Job for CronJob {
fn is_cron_job(&self) -> bool {
true
}
fn schedule(&self) -> Option<Cron> {
self.data.schedule()
}
fn repeated_every(&self) -> Option<u64> {
None
}
fn last_tick(&self) -> Option<DateTime<Utc>> {
self.data.last_tick_utc()
}
fn set_last_tick(&mut self, tick: Option<DateTime<Utc>>) {
self.data.set_last_tick(tick);
}
fn next_tick(&self) -> Option<DateTime<Utc>> {
self.data.next_tick_utc()
}
fn set_next_tick(&mut self, tick: Option<DateTime<Utc>>) {
self.data.set_next_tick(tick);
}
fn set_count(&mut self, count: u32) {
self.data.count = count;
}
fn count(&self) -> u32 {
self.data.count
}
fn increment_count(&mut self) {
self.data.count = if self.data.count + 1 < u32::MAX {
self.data.count + 1
} else {
0
}; // Overflow check
}
fn job_id(&self) -> Uuid {
self.data.id.as_ref().cloned().map(|e| e.into()).unwrap()
}
fn job_type(&self) -> JobType {
JobType::Cron
}
fn ran(&self) -> bool {
self.data.ran
}
fn set_ran(&mut self, ran: bool) {
self.data.ran = ran;
}
fn stop(&self) -> bool {
self.data.stopped
}
fn set_stopped(&mut self) {
self.data.stopped = true;
}
fn set_started(&mut self) {
self.data.stopped = false;
}
fn job_data_from_job(&mut self) -> Result<Option<JobStoredData>, JobSchedulerError> {
Ok(Some(self.data.clone()))
}
fn set_job_data(&mut self, job_data: JobStoredData) -> Result<(), JobSchedulerError> {
self.data = job_data;
Ok(())
}
fn run(&mut self, jobs: JobScheduler) -> Receiver<bool> {
let (tx, rx) = tokio::sync::oneshot::channel();
let job_id = self.job_id();
if !self.async_job {
(self.run)(job_id, jobs);
if let Err(e) = tx.send(true) {
error!("Error notifying done {:?}", e);
}
} else {
let future = (self.run_async)(job_id, jobs);
tokio::task::spawn(async move {
future.await;
if let Err(e) = tx.send(true) {
error!("Error notifying done {:?}", e);
}
});
}
rx
}
fn fixed_offset_west(&self) -> i32 {
self.data.time_offset_seconds
}
}
@@ -0,0 +1,90 @@
use crate::JobSchedulerError;
use crate::context::Context;
use crate::store::MetaDataStorage;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::broadcast::{Receiver, Sender};
use tracing::error;
use uuid::Uuid;
#[derive(Default)]
pub struct JobDeleter {}
impl JobDeleter {
async fn listen_to_removals(
storage: Arc<RwLock<Box<dyn MetaDataStorage + Send + Sync>>>,
mut rx: Receiver<Uuid>,
tx_deleted: Sender<Result<Uuid, (JobSchedulerError, Option<Uuid>)>>,
) {
loop {
let val = rx.recv().await;
if let Err(e) = val {
error!("Error receiving value {:?}", e);
break;
}
let uuid = val.unwrap();
{
let mut storage = storage.write().await;
let delete = storage.delete(uuid).await;
if let Err(e) = delete {
error!("Error deleting {:?}", e);
if let Err(e) = tx_deleted.send(Err((e, Some(uuid)))) {
error!("Error sending delete error {:?}", e);
}
continue;
}
}
if let Err(e) = tx_deleted.send(Ok(uuid)) {
error!("Error sending error {:?}", e);
}
}
}
pub fn init(
&mut self,
context: &Context,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send + Sync>> {
let rx = context.job_delete_tx.subscribe();
let tx_deleted = context.job_deleted_tx.clone();
let storage = context.metadata_storage.clone();
Box::pin(async move {
tokio::spawn(JobDeleter::listen_to_removals(storage, rx, tx_deleted));
Ok(())
})
}
pub async fn remove(context: &Context, job_id: &Uuid) -> Result<(), JobSchedulerError> {
let delete = context.job_delete_tx.clone();
let mut deleted = context.job_deleted_tx.subscribe();
let job_id = *job_id;
tokio::spawn(async move {
if let Err(e) = delete.send(job_id) {
error!("Error sending delete id {:?}", e);
}
});
while let Ok(deleted) = deleted.recv().await {
match deleted {
Ok(uuid) => {
if uuid == job_id {
return Ok(());
} else {
continue;
}
}
Err((e, Some(uuid))) => {
if uuid == job_id {
return Err(e);
} else {
continue;
}
}
_ => continue,
}
}
Err(JobSchedulerError::RemoveShutdownNotifier)
}
}
@@ -0,0 +1,178 @@
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct CronJob {
pub schedule: String,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct NonCronJob {
pub repeating: bool,
pub repeated_every: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Uuid {
pub id1: u64,
pub id2: u64,
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct JobStoredData {
pub id: ::core::option::Option<Uuid>,
pub last_updated: ::core::option::Option<u64>,
pub last_tick: ::core::option::Option<u64>,
pub next_tick: u64,
pub job_type: i32,
pub count: u32,
pub extra: Vec<u8>,
pub ran: bool,
pub stopped: bool,
pub job: ::core::option::Option<job_stored_data::Job>,
pub time_offset_seconds: i32,
}
/// Nested message and enum types in `JobStoredData`.
pub mod job_stored_data {
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
#[repr(i32)]
pub enum Job {
CronJob(super::CronJob),
NonCronJob(super::NonCronJob),
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct JobIdAndNotification {
pub job_id: ::core::option::Option<Uuid>,
pub notification_id: ::core::option::Option<Uuid>,
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct NotificationData {
pub job_id: ::core::option::Option<JobIdAndNotification>,
pub job_states: Vec<i32>,
pub extra: Vec<u8>,
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct NotificationIdAndState {
pub notification_id: ::core::option::Option<Uuid>,
pub job_state: i32,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct JobAndNextTick {
pub id: ::core::option::Option<Uuid>,
pub job_type: i32,
pub next_tick: u64,
pub last_tick: ::core::option::Option<u64>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct ListOfUuids {
pub uuids: Vec<Uuid>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct JobAndNotifications {
pub job_id: ::core::option::Option<Uuid>,
pub notification_ids: Vec<Uuid>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct ListOfJobsAndNotifications {
pub job_and_notifications: Vec<JobAndNotifications>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, FromPrimitive, ToPrimitive)]
#[repr(i32)]
pub enum JobState {
Stop = 0,
Scheduled = 1,
Started = 2,
Done = 3,
Removed = 4,
}
impl JobState {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
Self::Stop => "Stop",
Self::Scheduled => "Scheduled",
Self::Started => "Started",
Self::Done => "Done",
Self::Removed => "Removed",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"Stop" => Some(Self::Stop),
"Scheduled" => Some(Self::Scheduled),
"Started" => Some(Self::Started),
"Done" => Some(Self::Done),
"Removed" => Some(Self::Removed),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, FromPrimitive, ToPrimitive)]
#[repr(i32)]
pub enum JobType {
Cron = 0,
Repeated = 1,
OneShot = 2,
}
impl JobState {
pub fn from_i32(x: i32) -> Option<Self> {
match x {
0 => Some(Self::Stop),
1 => Some(Self::Scheduled),
2 => Some(Self::Started),
3 => Some(Self::Done),
4 => Some(Self::Removed),
_ => None,
}
}
}
impl JobType {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
Self::Cron => "Cron",
Self::Repeated => "Repeated",
Self::OneShot => "OneShot",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"Cron" => Some(Self::Cron),
"Repeated" => Some(Self::Repeated),
"OneShot" => Some(Self::OneShot),
_ => None,
}
}
pub fn from_i32(x: i32) -> Option<Self> {
match x {
0 => Some(Self::Cron),
1 => Some(Self::Repeated),
2 => Some(Self::OneShot),
_ => None,
}
}
}
impl From<JobState> for i32 {
fn from(val: JobState) -> Self {
val as i32
}
}
impl From<JobType> for i32 {
fn from(val: JobType) -> Self {
val as i32
}
}
impl JobStoredData {
pub fn job_type(&self) -> JobType {
JobType::from_i32(self.job_type).unwrap()
}
}
@@ -0,0 +1,170 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct CronJob {
#[prost(string, tag = "1")]
pub schedule: ::prost::alloc::string::String,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct NonCronJob {
#[prost(bool, tag = "1")]
pub repeating: bool,
#[prost(uint64, tag = "2")]
pub repeated_every: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Uuid {
#[prost(uint64, tag = "1")]
pub id1: u64,
#[prost(uint64, tag = "2")]
pub id2: u64,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct JobStoredData {
#[prost(message, optional, tag = "1")]
pub id: ::core::option::Option<Uuid>,
#[prost(uint64, optional, tag = "2")]
pub last_updated: ::core::option::Option<u64>,
#[prost(uint64, optional, tag = "3")]
pub last_tick: ::core::option::Option<u64>,
#[prost(uint64, tag = "4")]
pub next_tick: u64,
#[prost(enumeration = "JobType", tag = "5")]
pub job_type: i32,
#[prost(uint32, tag = "8")]
pub count: u32,
#[prost(bytes = "vec", tag = "9")]
pub extra: ::prost::alloc::vec::Vec<u8>,
#[prost(bool, tag = "10")]
pub ran: bool,
#[prost(bool, tag = "11")]
pub stopped: bool,
#[prost(int32, tag = "12")]
pub time_offset_seconds: i32,
#[prost(oneof = "job_stored_data::Job", tags = "6, 7")]
pub job: ::core::option::Option<job_stored_data::Job>,
}
/// Nested message and enum types in `JobStoredData`.
pub mod job_stored_data {
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)]
pub enum Job {
#[prost(message, tag = "6")]
CronJob(super::CronJob),
#[prost(message, tag = "7")]
NonCronJob(super::NonCronJob),
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct JobIdAndNotification {
#[prost(message, optional, tag = "1")]
pub job_id: ::core::option::Option<Uuid>,
#[prost(message, optional, tag = "2")]
pub notification_id: ::core::option::Option<Uuid>,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct NotificationData {
#[prost(message, optional, tag = "1")]
pub job_id: ::core::option::Option<JobIdAndNotification>,
#[prost(enumeration = "JobState", repeated, tag = "2")]
pub job_states: ::prost::alloc::vec::Vec<i32>,
#[prost(bytes = "vec", tag = "3")]
pub extra: ::prost::alloc::vec::Vec<u8>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct NotificationIdAndState {
#[prost(message, optional, tag = "1")]
pub notification_id: ::core::option::Option<Uuid>,
#[prost(enumeration = "JobState", tag = "2")]
pub job_state: i32,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct JobAndNextTick {
#[prost(message, optional, tag = "1")]
pub id: ::core::option::Option<Uuid>,
#[prost(enumeration = "JobType", tag = "2")]
pub job_type: i32,
#[prost(uint64, tag = "3")]
pub next_tick: u64,
#[prost(uint64, optional, tag = "4")]
pub last_tick: ::core::option::Option<u64>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ListOfUuids {
#[prost(message, repeated, tag = "1")]
pub uuids: ::prost::alloc::vec::Vec<Uuid>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct JobAndNotifications {
#[prost(message, optional, tag = "1")]
pub job_id: ::core::option::Option<Uuid>,
#[prost(message, repeated, tag = "2")]
pub notification_ids: ::prost::alloc::vec::Vec<Uuid>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ListOfJobsAndNotifications {
#[prost(message, repeated, tag = "1")]
pub job_and_notifications: ::prost::alloc::vec::Vec<JobAndNotifications>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum JobState {
Stop = 0,
Scheduled = 1,
Started = 2,
Done = 3,
Removed = 4,
}
impl JobState {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
Self::Stop => "Stop",
Self::Scheduled => "Scheduled",
Self::Started => "Started",
Self::Done => "Done",
Self::Removed => "Removed",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"Stop" => Some(Self::Stop),
"Scheduled" => Some(Self::Scheduled),
"Started" => Some(Self::Started),
"Done" => Some(Self::Done),
"Removed" => Some(Self::Removed),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum JobType {
Cron = 0,
Repeated = 1,
OneShot = 2,
}
impl JobType {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
Self::Cron => "Cron",
Self::Repeated => "Repeated",
Self::OneShot => "OneShot",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"Cron" => Some(Self::Cron),
"Repeated" => Some(Self::Repeated),
"OneShot" => Some(Self::OneShot),
_ => None,
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,139 @@
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::{JobStoredData, JobType};
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::{JobStoredData, JobType};
use crate::job::{Job, JobToRunAsync};
use crate::{JobScheduler, JobSchedulerError, JobToRun};
use chrono::{DateTime, Utc};
use croner::Cron;
use tokio::sync::oneshot::Receiver;
use tracing::error;
use uuid::Uuid;
pub struct NonCronJob {
pub run: Box<JobToRun>,
pub run_async: Box<JobToRunAsync>,
pub data: JobStoredData,
pub async_job: bool,
}
impl Job for NonCronJob {
fn is_cron_job(&self) -> bool {
false
}
fn schedule(&self) -> Option<Cron> {
None
}
#[cfg(feature = "has_bytes")]
fn repeated_every(&self) -> Option<u64> {
self.data.job.as_ref().and_then(|jt| match jt {
crate::job::job_data_prost::job_stored_data::Job::CronJob(_) => None,
crate::job::job_data_prost::job_stored_data::Job::NonCronJob(ncj) => {
Some(ncj.repeated_every)
}
})
}
#[cfg(not(feature = "has_bytes"))]
fn repeated_every(&self) -> Option<u64> {
self.data.job.as_ref().and_then(|jt| match jt {
crate::job::job_data::job_stored_data::Job::CronJob(_) => None,
crate::job::job_data::job_stored_data::Job::NonCronJob(ncj) => Some(ncj.repeated_every),
})
}
fn last_tick(&self) -> Option<DateTime<Utc>> {
self.data.last_tick_utc()
}
fn set_last_tick(&mut self, tick: Option<DateTime<Utc>>) {
self.data.set_last_tick(tick);
}
fn next_tick(&self) -> Option<DateTime<Utc>> {
self.data.next_tick_utc()
}
fn set_next_tick(&mut self, tick: Option<DateTime<Utc>>) {
self.data.set_next_tick(tick)
}
fn set_count(&mut self, count: u32) {
self.data.count = count;
}
fn count(&self) -> u32 {
self.data.count
}
fn increment_count(&mut self) {
self.data.count = if self.data.count + 1 < u32::MAX {
self.data.count + 1
} else {
0
}; // Overflow check
}
fn job_id(&self) -> Uuid {
self.data.id.as_ref().cloned().map(|e| e.into()).unwrap()
}
fn job_type(&self) -> JobType {
self.data.job_type()
}
fn ran(&self) -> bool {
self.data.ran
}
fn set_ran(&mut self, ran: bool) {
self.data.ran = ran;
}
fn stop(&self) -> bool {
self.data.stopped
}
fn set_stopped(&mut self) {
self.data.stopped = true;
}
fn set_started(&mut self) {
self.data.stopped = false;
}
fn job_data_from_job(&mut self) -> Result<Option<JobStoredData>, JobSchedulerError> {
Ok(Some(self.data.clone()))
}
fn set_job_data(&mut self, job_data: JobStoredData) -> Result<(), JobSchedulerError> {
self.data = job_data;
Ok(())
}
fn run(&mut self, jobs: JobScheduler) -> Receiver<bool> {
let (tx, rx) = tokio::sync::oneshot::channel();
let job_id = self.job_id();
if !self.async_job {
(self.run)(job_id, jobs);
if let Err(e) = tx.send(true) {
error!("Error notifying done {:?}", e);
}
} else {
let future = (self.run_async)(job_id, jobs);
tokio::task::spawn(async move {
future.await;
if let Err(e) = tx.send(true) {
error!("Error notifying done {:?}", e);
}
});
}
rx
}
fn fixed_offset_west(&self) -> i32 {
self.data.time_offset_seconds
}
}
@@ -0,0 +1,87 @@
use crate::JobSchedulerError;
use crate::context::Context;
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::JobState;
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::JobState;
use crate::job::to_code::JobCode;
use crate::job_scheduler::JobsSchedulerLocked;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::broadcast::error::RecvError;
use tokio::sync::broadcast::{Receiver, Sender};
use tracing::error;
use uuid::Uuid;
#[derive(Default)]
pub struct JobRunner {}
impl JobRunner {
async fn listen_for_activations(
job_code: Arc<RwLock<Box<dyn JobCode + Send + Sync>>>,
mut rx: Receiver<Uuid>,
tx_notify: Sender<(Uuid, JobState)>,
job_scheduler: JobsSchedulerLocked,
) {
loop {
let val = rx.recv().await;
if let Err(e) = val {
error!("Error receiving {:?}", e);
if matches!(e, RecvError::Closed) {
break;
}
continue;
}
let uuid = val.unwrap();
{
let tx = tx_notify.clone();
tokio::spawn(async move {
if let Err(e) = tx.send((uuid, JobState::Started)) {
error!("Error sending error listening for activation {:?}", e);
}
});
}
let mut w = job_code.write().await;
let code = w.get(uuid).await;
match code {
Ok(Some(job)) => {
let mut job = job.write().await;
let v = (job)(uuid, job_scheduler.clone());
let tx = tx_notify.clone();
tokio::spawn(async move {
v.await;
if let Err(e) = tx.send((uuid, JobState::Done)) {
error!("Error sending spawned task {:?}", e);
}
});
}
_ => {
error!("Error getting {:?} from job code", uuid);
continue;
}
}
}
}
pub fn init(
&mut self,
context: &Context,
job_scheduler: JobsSchedulerLocked,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let job_code = context.job_code.clone();
let notify_tx = context.notify_tx.clone();
let job_activation_rx = context.job_activation_tx.subscribe();
Box::pin(async move {
tokio::spawn(JobRunner::listen_for_activations(
job_code,
job_activation_rx,
notify_tx,
job_scheduler,
));
Ok(())
})
}
}
@@ -0,0 +1,27 @@
use crate::context::Context;
use crate::job::JobToRunAsync;
use crate::{JobSchedulerError, OnJobNotification};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
pub type PinnedGetFuture<T> =
Pin<Box<dyn Future<Output = Result<Option<Arc<RwLock<T>>>, JobSchedulerError>> + Send>>;
pub trait ToCode<T>
where
T: Send,
{
fn init(
&mut self,
context: &Context,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>>;
fn get(&mut self, uuid: Uuid) -> PinnedGetFuture<T>;
}
pub trait JobCode: ToCode<Box<JobToRunAsync>> + Send {}
pub trait NotificationCode: ToCode<Box<OnJobNotification>> {}
@@ -0,0 +1,446 @@
use crate::context::Context;
use crate::error::JobSchedulerError;
use crate::job::to_code::{JobCode, NotificationCode};
use crate::job::{JobCreator, JobDeleter, JobLocked, JobRunner};
use crate::notification::{NotificationCreator, NotificationDeleter, NotificationRunner};
use crate::scheduler::Scheduler;
use crate::simple::{
SimpleJobCode, SimpleMetadataStore, SimpleNotificationCode, SimpleNotificationStore,
};
use crate::store::{MetaDataStorage, NotificationStore};
use chrono::{DateTime, NaiveDateTime, Utc};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(all(unix, feature = "signal"))]
use tokio::signal::unix::SignalKind;
use tokio::sync::RwLock;
use tracing::{error, info};
use uuid::Uuid;
pub type ShutdownNotification =
dyn FnMut() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync;
/// The JobScheduler contains and executes the scheduled jobs.
pub struct JobsSchedulerLocked {
pub context: Arc<Context>,
pub inited: Arc<AtomicBool>,
pub job_creator: Arc<RwLock<JobCreator>>,
pub job_deleter: Arc<RwLock<JobDeleter>>,
pub job_runner: Arc<RwLock<JobRunner>>,
pub notification_creator: Arc<RwLock<NotificationCreator>>,
pub notification_deleter: Arc<RwLock<NotificationDeleter>>,
pub notification_runner: Arc<RwLock<NotificationRunner>>,
pub scheduler: Arc<RwLock<Scheduler>>,
pub shutdown_notifier: Option<Arc<RwLock<Box<ShutdownNotification>>>>,
}
impl Clone for JobsSchedulerLocked {
fn clone(&self) -> Self {
JobsSchedulerLocked {
context: self.context.clone(),
inited: self.inited.clone(),
job_creator: self.job_creator.clone(),
job_deleter: self.job_deleter.clone(),
job_runner: self.job_runner.clone(),
notification_creator: self.notification_creator.clone(),
notification_deleter: self.notification_deleter.clone(),
notification_runner: self.notification_runner.clone(),
scheduler: self.scheduler.clone(),
shutdown_notifier: self.shutdown_notifier.clone(),
}
}
}
impl JobsSchedulerLocked {
async fn init_context(
metadata_storage: Arc<RwLock<Box<dyn MetaDataStorage + Send + Sync>>>,
notification_storage: Arc<RwLock<Box<dyn NotificationStore + Send + Sync>>>,
job_code: Arc<RwLock<Box<dyn JobCode + Send + Sync>>>,
notify_code: Arc<RwLock<Box<dyn NotificationCode + Send + Sync>>>,
channel_size: usize,
) -> Result<Arc<Context>, JobSchedulerError> {
{
let mut metadata_storage = metadata_storage.write().await;
metadata_storage.init().await?;
}
{
let mut notification_storage = notification_storage.write().await;
notification_storage.init().await?;
}
let context = Context::new_with_channel_size(
metadata_storage,
notification_storage,
job_code.clone(),
notify_code.clone(),
channel_size,
);
{
let mut job_code = job_code.write().await;
job_code.init(&context).await?;
}
{
let mut notification_code = notify_code.write().await;
notification_code.init(&context).await?;
}
Ok(Arc::new(context))
}
async fn init_actors(self) -> Result<(), JobSchedulerError> {
let for_job_runner = self.clone();
let Self {
context,
job_creator,
job_deleter,
job_runner,
notification_creator,
notification_deleter,
notification_runner,
scheduler,
..
} = self;
{
let job_creator = job_creator.write().await;
job_creator.init(&context).await?;
}
{
let mut job_deleter = job_deleter.write().await;
job_deleter.init(&context).await?;
}
{
let mut notification_creator = notification_creator.write().await;
notification_creator.init(&context).await?;
}
{
let mut notification_deleter = notification_deleter.write().await;
notification_deleter.init(&context).await?;
}
{
let mut notification_runner = notification_runner.write().await;
notification_runner.init(&context).await?;
}
{
let mut runner = job_runner.write().await;
runner.init(&context, for_job_runner).await?;
}
{
let mut scheduler = scheduler.write().await;
scheduler.init(&context).await;
}
Ok(())
}
///
/// Get whether the scheduler is initialized
pub async fn inited(&self) -> bool {
self.inited.load(Ordering::Relaxed)
}
///
/// Initialize the actors
pub async fn init(&mut self) -> Result<(), JobSchedulerError> {
if self.inited().await {
return Ok(());
}
self.inited.swap(true, Ordering::Relaxed);
self.clone()
.init_actors()
.await
.map_err(|_| JobSchedulerError::CantInit)
}
///
/// Create a new `MetaDataStorage` and `NotificationStore` using the `SimpleMetadataStore`, `SimpleNotificationStore`,
/// `SimpleJobCode` and `SimpleNotificationCode` implementation with channel size of 200
pub async fn new() -> Result<Self, JobSchedulerError> {
Self::new_with_channel_size(200).await
}
///
/// Create a new `MetaDataStorage` and `NotificationStore` using the `SimpleMetadataStore`, `SimpleNotificationStore`,
/// `SimpleJobCode` and `SimpleNotificationCode` implementation
///
/// The channel_size parameter is used to set the size of the channels used to communicate between the actors.
/// The amount in short affects how many messages can be buffered before the sender is blocked.
/// When the sender is blocked, the processing is lagged.
pub async fn new_with_channel_size(channel_size: usize) -> Result<Self, JobSchedulerError> {
let metadata_storage = SimpleMetadataStore::default();
let metadata_storage: Arc<RwLock<Box<dyn MetaDataStorage + Send + Sync>>> =
Arc::new(RwLock::new(Box::new(metadata_storage)));
let notification_storage = SimpleNotificationStore::default();
let notification_storage: Arc<RwLock<Box<dyn NotificationStore + Send + Sync>>> =
Arc::new(RwLock::new(Box::new(notification_storage)));
let job_code = SimpleJobCode::default();
let job_code: Arc<RwLock<Box<dyn JobCode + Send + Sync>>> =
Arc::new(RwLock::new(Box::new(job_code)));
let notify_code = SimpleNotificationCode::default();
let notify_code: Arc<RwLock<Box<dyn NotificationCode + Send + Sync>>> =
Arc::new(RwLock::new(Box::new(notify_code)));
let context = JobsSchedulerLocked::init_context(
metadata_storage,
notification_storage,
job_code,
notify_code,
channel_size,
)
.await
.map_err(|_| JobSchedulerError::CantInit)?;
let val = JobsSchedulerLocked {
context,
inited: Arc::new(AtomicBool::new(false)),
job_creator: Arc::new(Default::default()),
job_deleter: Arc::new(Default::default()),
job_runner: Arc::new(Default::default()),
notification_creator: Arc::new(Default::default()),
notification_deleter: Arc::new(Default::default()),
notification_runner: Arc::new(Default::default()),
scheduler: Arc::new(Default::default()),
shutdown_notifier: None,
};
Ok(val)
}
///
/// Create a new `JobsSchedulerLocked` using custom metadata and notification runners, job and notification
/// code providers
pub async fn new_with_storage_and_code(
metadata_storage: Box<dyn MetaDataStorage + Send + Sync>,
notification_storage: Box<dyn NotificationStore + Send + Sync>,
job_code: Box<dyn JobCode + Send + Sync>,
notification_code: Box<dyn NotificationCode + Send + Sync>,
channel_size: usize,
) -> Result<Self, JobSchedulerError> {
let metadata_storage = Arc::new(RwLock::new(metadata_storage));
let notification_storage = Arc::new(RwLock::new(notification_storage));
let job_code = Arc::new(RwLock::new(job_code));
let notification_code = Arc::new(RwLock::new(notification_code));
let context = JobsSchedulerLocked::init_context(
metadata_storage,
notification_storage,
job_code,
notification_code,
channel_size,
)
.await?;
let val = JobsSchedulerLocked {
context,
inited: Arc::new(AtomicBool::new(false)),
job_creator: Arc::new(Default::default()),
job_deleter: Arc::new(Default::default()),
job_runner: Arc::new(Default::default()),
notification_creator: Arc::new(Default::default()),
notification_deleter: Arc::new(Default::default()),
notification_runner: Arc::new(Default::default()),
scheduler: Arc::new(Default::default()),
shutdown_notifier: None,
};
Ok(val)
}
/// Add a job to the `JobScheduler`
///
/// ```rust,ignore
/// use tokio_cron_scheduler::{Job, JobScheduler, JobToRun};
/// let mut sched = JobScheduler::new();
/// sched.add(Job::new("1/10 * * * * *".parse().unwrap(), || {
/// println!("I get executed every 10 seconds!");
/// })).await;
/// ```
pub async fn add(&self, job: JobLocked) -> Result<Uuid, JobSchedulerError> {
let guid = job.guid();
if !self.inited().await {
info!("Uninited");
let mut s = self.clone();
s.init().await?;
}
let context = self.context.clone();
JobCreator::add(&context, job).await?;
info!("Job creator created");
Ok(guid)
}
/// Remove a job from the `JobScheduler`
///
/// ```rust,ignore
/// use tokio_cron_scheduler::{Job, JobScheduler, JobToRun};
/// let mut sched = JobScheduler::new();
/// let job_id = sched.add(Job::new("1/10 * * * * *".parse().unwrap(), || {
/// println!("I get executed every 10 seconds!");
/// }))?.await;
/// sched.remove(job_id).await;
/// ```
///
/// Note, the UUID of the job can be fetched calling .guid() on a Job.
///
pub async fn remove(&self, to_be_removed: &Uuid) -> Result<(), JobSchedulerError> {
if !self.inited().await {
let mut s = self.clone();
s.init().await?;
}
let context = self.context();
JobDeleter::remove(&context, to_be_removed).await
}
/// The `start` spawns a Tokio task where it loops. Every 500ms it
/// runs the tick method to increment any pending jobs.
///
/// ```rust,ignore
/// if let Err(e) = sched.start().await {
/// eprintln!("Error on scheduler {:?}", e);
/// }
/// ```
pub async fn start(&self) -> Result<(), JobSchedulerError> {
if !self.inited().await {
let mut s = self.clone();
s.init().await?;
}
let mut scheduler = self.scheduler.write().await;
let ret = scheduler.start().await;
match ret {
Ok(ret) => Ok(ret),
Err(e) => {
error!("Error receiving start result {:?}", e);
Err(JobSchedulerError::StartScheduler)
}
}
}
/// The `time_till_next_job` method returns the duration till the next job
/// is supposed to run. This can be used to sleep until then without waking
/// up at a fixed interval.AsMut
/// ```
pub async fn time_till_next_job(
&mut self,
) -> Result<Option<std::time::Duration>, JobSchedulerError> {
if !self.inited().await {
let mut s = self.clone();
s.init().await?;
}
let metadata = self.context.metadata_storage.clone();
let mut metadata = metadata.write().await;
let ret = metadata.time_till_next_job().await;
match ret {
Ok(ret) => Ok(ret),
Err(e) => {
error!("Error getting return of time till next job {:?}", e);
Err(JobSchedulerError::CantGetTimeUntil)
}
}
}
/// `next_tick_for_job` returns the date/time for when the next tick will
/// be for a job
pub async fn next_tick_for_job(
&mut self,
job_id: Uuid,
) -> Result<Option<DateTime<Utc>>, JobSchedulerError> {
if !self.inited().await {
let mut s = self.clone();
s.init().await?;
}
let mut r = self.context.metadata_storage.write().await;
r.get(job_id).await.map(|v| {
if let Some(vv) = v {
if vv.next_tick == 0 {
return None;
}
match NaiveDateTime::from_timestamp_opt(vv.next_tick as i64, 0) {
None => None,
Some(ts) => Some(DateTime::from_naive_utc_and_offset(ts, Utc)),
}
} else {
None
}
})
}
///
/// Shut the scheduler down
pub async fn shutdown(&mut self) -> Result<(), JobSchedulerError> {
let mut notify = None;
std::mem::swap(&mut self.shutdown_notifier, &mut notify);
let mut scheduler = self.scheduler.write().await;
scheduler.shutdown().await;
if let Some(notify) = notify {
let mut notify = notify.write().await;
notify().await;
}
Ok(())
}
///
/// Wait for a signal to shut the runtime down with
#[cfg(all(unix, feature = "signal"))]
pub fn shutdown_on_signal(&self, signal: SignalKind) {
let mut l = self.clone();
tokio::spawn(async move {
if let Some(_k) = tokio::signal::unix::signal(signal)
.expect("Can't wait for signal")
.recv()
.await
{
l.shutdown().await.expect("Problem shutting down");
}
});
}
///
/// Wait for a signal to shut the runtime down with
#[cfg(feature = "signal")]
pub fn shutdown_on_ctrl_c(&self) {
let mut l = self.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c()
.await
.expect("Could not await ctrl-c");
if let Err(err) = l.shutdown().await {
error!("{:?}", err);
}
});
}
///
/// Code that is run after the shutdown was run
pub fn set_shutdown_handler(&mut self, job: Box<ShutdownNotification>) {
self.shutdown_notifier = Some(Arc::new(RwLock::new(job)));
}
///
/// Remove the shutdown handler
pub fn remove_shutdown_handler(&mut self) {
self.shutdown_notifier = None;
}
///
/// Get the context
pub fn context(&self) -> Arc<Context> {
self.context.clone()
}
}
+182
View File
@@ -0,0 +1,182 @@
#[cfg(not(feature = "has_bytes"))]
#[macro_use]
extern crate num_derive;
extern crate core;
mod context;
mod error;
pub mod job;
mod job_scheduler;
#[cfg(feature = "nats_storage")]
mod nats;
mod notification;
#[cfg(feature = "postgres_storage")]
mod postgres;
mod scheduler;
mod simple;
pub mod store;
use std::ops::Add;
use std::time::{Duration, SystemTime};
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::ListOfUuids;
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::ListOfUuids;
use chrono::{DateTime, Utc};
use croner::Cron;
use croner::parser::CronParser;
#[cfg(not(feature = "has_bytes"))]
use job::job_data::{JobAndNextTick, JobStoredData, Uuid as JobUuid};
#[cfg(feature = "has_bytes")]
use job::job_data_prost::{JobAndNextTick, JobStoredData, Uuid as JobUuid};
use uuid::Uuid;
#[cfg(feature = "nats_storage")]
pub use crate::nats::{NatsMetadataStore, NatsNotificationStore, NatsStore, NatsStoreBuilder};
#[cfg(feature = "postgres_storage")]
pub use crate::postgres::{PostgresMetadataStore, PostgresNotificationStore, PostgresStore};
pub use context::Context;
pub use error::JobSchedulerError;
pub use job::JobLocked as Job;
pub use job::OnJobNotification;
#[cfg(not(feature = "has_bytes"))]
pub use job::job_data::JobState as JobNotification;
#[cfg(feature = "has_bytes")]
pub use job::job_data_prost::JobState as JobNotification;
pub use job::to_code::{JobCode, NotificationCode, PinnedGetFuture, ToCode};
pub use job::{JobBuilder, JobToRun, JobToRunAsync};
pub use job_scheduler::JobsSchedulerLocked as JobScheduler;
pub use store::{MetaDataStorage, NotificationStore};
pub use simple::{
SimpleJobCode, SimpleMetadataStore, SimpleNotificationCode, SimpleNotificationStore,
};
impl JobUuid {
pub fn from_u128(uuid: u128) -> Self {
let id1 = (uuid >> 64) as u64;
let id2 = (uuid & 0xFFFF_FFFF_FFFF_FFFF) as u64;
Self { id1, id2 }
}
pub fn as_u128(&self) -> u128 {
((self.id1 as u128) << 64) + (self.id2 as u128)
}
}
impl From<Uuid> for JobUuid {
fn from(uuid: Uuid) -> Self {
JobUuid::from_u128(uuid.as_u128())
}
}
impl From<&Uuid> for JobUuid {
fn from(uuid: &Uuid) -> Self {
JobUuid::from_u128(uuid.as_u128())
}
}
impl From<JobUuid> for Uuid {
fn from(uuid: JobUuid) -> Self {
Uuid::from_u128(uuid.as_u128())
}
}
impl From<&JobUuid> for Uuid {
fn from(uuid: &JobUuid) -> Self {
Uuid::from_u128(uuid.as_u128())
}
}
impl JobAndNextTick {
pub fn utc(lt: u64) -> DateTime<Utc> {
let dt = SystemTime::UNIX_EPOCH.add(Duration::from_secs(lt));
let dt: DateTime<Utc> = DateTime::from(dt);
dt
}
fn next_tick_utc(&self) -> Option<DateTime<Utc>> {
match self.next_tick {
0 => None,
val => Some(JobAndNextTick::utc(val)),
}
}
fn last_tick_utc(&self) -> Option<DateTime<Utc>> {
self.last_tick.map(JobAndNextTick::utc)
}
}
impl JobStoredData {
pub fn schedule(&self) -> Option<Cron> {
self.job
.as_ref()
.and_then(|j| match j {
#[cfg(feature = "has_bytes")]
job::job_data_prost::job_stored_data::Job::CronJob(cj) => Some(&*cj.schedule),
#[cfg(not(feature = "has_bytes"))]
job::job_data::job_stored_data::Job::CronJob(cj) => Some(&*cj.schedule),
_ => None,
})
.and_then(|s| {
CronParser::builder()
.seconds(croner::parser::Seconds::Required)
.dom_and_dow(true)
.build()
.parse(s)
.ok()
})
}
pub fn next_tick_utc(&self) -> Option<DateTime<Utc>> {
match self.next_tick {
0 => None,
val => Some(JobAndNextTick::utc(val)),
}
}
pub fn last_tick_utc(&self) -> Option<DateTime<Utc>> {
self.last_tick.map(JobAndNextTick::utc)
}
pub fn repeated_every(&self) -> Option<u64> {
self.job.as_ref().and_then(|jt| match jt {
#[cfg(feature = "has_bytes")]
job::job_data_prost::job_stored_data::Job::CronJob(_) => None,
#[cfg(not(feature = "has_bytes"))]
job::job_data::job_stored_data::Job::CronJob(_) => None,
#[cfg(feature = "has_bytes")]
job::job_data_prost::job_stored_data::Job::NonCronJob(ncj) => Some(ncj.repeated_every),
#[cfg(not(feature = "has_bytes"))]
job::job_data::job_stored_data::Job::NonCronJob(ncj) => Some(ncj.repeated_every),
})
}
pub fn set_next_tick(&mut self, tick: Option<DateTime<Utc>>) {
self.next_tick = match tick {
Some(t) => t.timestamp() as u64,
None => 0,
}
}
pub fn set_last_tick(&mut self, tick: Option<DateTime<Utc>>) {
self.last_tick = tick.map(|t| t.timestamp() as u64);
}
}
impl ListOfUuids {
// Allowing dead code for non-Nats library users.
#[allow(dead_code)]
pub fn uuid_in_list(&self, uuid: Uuid) -> bool {
self.uuids
.iter()
.map(|uuid| {
let uuid: Uuid = uuid.into();
uuid
})
.any(|val| val == uuid)
}
}
@@ -0,0 +1,365 @@
use crate::job::job_data_prost::ListOfUuids;
use crate::nats::{NatsStore, sanitize_nats_key};
use crate::store::{DataStore, InitStore, MetaDataStorage};
use crate::{JobAndNextTick, JobSchedulerError, JobStoredData, JobUuid};
use async_nats::jetstream::kv::Store;
use bytes::Bytes;
use chrono::{DateTime, Utc};
use prost::Message;
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use tokio::sync::RwLockReadGuard;
use tracing::error;
use uuid::Uuid;
const LIST_NAME: &str = "TCS_JOB_LIST";
const METADATA_PRE: &str = "META_";
///
/// A Nats KV store backed metadata store
#[derive(Clone)]
pub struct NatsMetadataStore {
pub store: NatsStore,
}
fn uuid_to_nats_id(uuid: Uuid) -> String {
let uuid = METADATA_PRE.to_string() + &*uuid.to_string();
sanitize_nats_key(&*uuid)
}
impl DataStore<JobStoredData> for NatsMetadataStore {
fn get(
&mut self,
id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<Option<JobStoredData>, JobSchedulerError>> + Send>>
{
let bucket = self.store.bucket.clone();
Box::pin(async move {
let r = bucket.read().await;
let id = uuid_to_nats_id(id);
r.get(&*id)
.await
.map_err(|e| {
error!("Error getting data {:?}", e);
JobSchedulerError::GetJobData
})
.map(|v| v.and_then(|v| JobStoredData::decode(v).ok()))
})
}
fn add_or_update(
&mut self,
data: JobStoredData,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let bucket = self.store.bucket.clone();
let uuid: Uuid = data.id.as_ref().unwrap().into();
let get = self.get(uuid);
let add_to_list = self.add_to_list_of_guids(uuid);
Box::pin(async move {
let bucket = bucket.read().await;
let bytes = data.encode_to_vec();
let prev = get.await;
let uuid = uuid_to_nats_id(uuid);
let done = match prev {
Ok(Some(_)) => bucket.put(&*uuid, Bytes::from(bytes)).await.map_err(|_| ()),
Ok(None) => bucket
.create(&*uuid, Bytes::from(bytes))
.await
.map_err(|_| ()),
Err(e) => {
error!(
"Error getting existing value {:?}, assuming does not exist and hope for the best",
e
);
bucket
.create(&*uuid, Bytes::from(bytes))
.await
.map_err(|_| ())
}
};
let added = add_to_list.await;
match (done, added) {
(Ok(_), Ok(_)) => Ok(()),
_ => Err(JobSchedulerError::CantAdd),
}
})
}
fn delete(
&mut self,
guid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let bucket = self.store.bucket.clone();
let removed_from_list = self.remove_from_list(guid);
Box::pin(async move {
let bucket = bucket.read().await;
let guid = uuid_to_nats_id(guid);
let deleted = bucket.delete(&*guid).await;
let removed_from_list = removed_from_list.await;
match (deleted, removed_from_list) {
(Ok(_), Ok(_)) => Ok(()),
_ => Err(JobSchedulerError::CantRemove),
}
})
}
}
impl InitStore for NatsMetadataStore {
fn init(&mut self) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
Box::pin(async move {
// Nop
// That being said. Would've been better to do the connection startup here.
Ok(())
})
}
fn inited(&mut self) -> Pin<Box<dyn Future<Output = Result<bool, JobSchedulerError>> + Send>> {
let inited = self.store.inited;
Box::pin(async move { Ok(inited) })
}
}
impl MetaDataStorage for NatsMetadataStore {
fn list_next_ticks(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<Vec<JobAndNextTick>, JobSchedulerError>> + Send>> {
let list_guids = self.list_guids();
let bucket = self.store.bucket.clone();
Box::pin(async move {
let list = list_guids.await;
if let Err(e) = list {
error!("Error getting list of guids {:?}", e);
return Err(e);
}
let list = list.unwrap();
let bucket = bucket.read().await;
let mut ret = vec![];
for uuid in list.uuids {
let uuid: Uuid = uuid.into();
let jd = bucket
.get(&*uuid_to_nats_id(uuid))
.await
.ok()
.flatten()
.map(|buf| JobStoredData::decode(buf).ok())
.flatten();
if let Some(jd) = jd {
ret.push(JobAndNextTick {
id: jd.id,
job_type: jd.job_type,
next_tick: jd.next_tick,
last_tick: jd.last_tick,
});
}
}
Ok(ret)
})
}
fn set_next_and_last_tick(
&mut self,
guid: Uuid,
next_tick: Option<DateTime<Utc>>,
last_tick: Option<DateTime<Utc>>,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let get = self.get(guid);
let bucket = self.store.bucket.clone();
Box::pin(async move {
let get = get.await;
match get {
Ok(Some(mut val)) => {
val.next_tick = match next_tick {
Some(next_tick) => next_tick.timestamp(),
None => 0,
} as u64;
val.last_tick = last_tick.map(|lt| lt.timestamp() as u64);
let bytes = val.encode_to_vec();
let bucket = bucket.read().await;
bucket
.put(&*uuid_to_nats_id(guid), Bytes::from(bytes))
.await
.map(|_| ())
.map_err(|e| {
error!("Error updating value {:?}", e);
JobSchedulerError::UpdateJobData
})
}
Ok(None) => {
error!("Could not get value to update");
Err(JobSchedulerError::UpdateJobData)
}
Err(e) => {
error!("Could not get value to update {:?}", e);
Err(JobSchedulerError::UpdateJobData)
}
}
})
}
fn time_till_next_job(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<Option<Duration>, JobSchedulerError>> + Send>> {
let list = self.list_guids();
let bucket = self.store.bucket.clone();
Box::pin(async move {
let list = list.await;
if let Err(e) = list {
error!("Could not get list of guids {:?}", e);
return Err(JobSchedulerError::CantGetTimeUntil);
}
let list = list.unwrap();
let bucket = bucket.read().await;
let now = Utc::now();
let now = now.timestamp() as u64;
let mut jd_list = vec![];
for uuid in list.uuids {
let uuid: Uuid = uuid.into();
let jd = bucket
.get(&*uuid_to_nats_id(uuid))
.await
.ok()
.flatten()
.map(|b| JobStoredData::decode(b).ok())
.flatten();
if let Some(jd) = jd {
jd_list.push(jd);
}
}
let ret = jd_list
.iter()
.filter_map(|jd| match jd.next_tick {
0 => None,
i => {
if i > now {
Some(i)
} else {
None
}
}
})
.min()
.map(|t| t - now)
.map(std::time::Duration::from_secs);
Ok(ret)
})
}
}
impl NatsMetadataStore {
pub async fn default() -> Self {
let store = NatsStore::default().await;
Self { store }
}
fn list_guids(
&self,
) -> Pin<Box<dyn Future<Output = Result<ListOfUuids, JobSchedulerError>> + Send>> {
let bucket = self.store.bucket.clone();
Box::pin(async move {
let r = bucket.read().await;
let list = r.get(&*sanitize_nats_key(LIST_NAME)).await;
match list {
Ok(Some(list)) => ListOfUuids::decode(list).map_err(|e| {
error!("Error decoding list value {:?}", e);
JobSchedulerError::CantListGuids
}),
Ok(None) => Ok(ListOfUuids::default()),
Err(e) => {
error!("Error getting list of guids {:?}", e);
Err(JobSchedulerError::CantListGuids)
}
}
})
}
fn add_to_list_of_guids(
&self,
uuid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let list = self.list_guids();
let bucket = self.store.bucket.clone();
Box::pin(async move {
let list = list.await;
if let Err(e) = list {
error!("Could not get list of guids {:?}", e);
return Err(JobSchedulerError::ErrorLoadingGuidList);
}
let mut list = list.unwrap();
let exists = list.uuid_in_list(uuid);
if exists {
return Ok(());
}
let uuid: JobUuid = uuid.into();
list.uuids.push(uuid);
let bucket = bucket.read().await;
NatsMetadataStore::update_list(bucket, list).await
})
}
async fn update_list(
bucket: RwLockReadGuard<'_, Store>,
list: ListOfUuids,
) -> Result<(), JobSchedulerError> {
let has_list_already = bucket
.get(&*sanitize_nats_key(LIST_NAME))
.await
.ok()
.flatten()
.is_some();
if has_list_already {
bucket
.put(
&*sanitize_nats_key(LIST_NAME),
Bytes::from(list.encode_to_vec()),
)
.await
.map_err(|e| {
error!("Error saving list of guids {:?}", e);
JobSchedulerError::CantAdd
})
} else {
bucket
.create(
&*sanitize_nats_key(LIST_NAME),
Bytes::from(list.encode_to_vec()),
)
.await
.map_err(|e| {
error!("Error saving list of guids {:?}", e);
JobSchedulerError::CantAdd
})
}
.map(|_| ())
}
fn remove_from_list(
&self,
uuid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let list = self.list_guids();
let bucket = self.store.bucket.clone();
Box::pin(async move {
let list = list.await;
if let Err(e) = list {
error!("Could not get list of guids {:?}", e);
return Err(JobSchedulerError::ErrorLoadingGuidList);
}
let mut list = list.unwrap();
let exists = list.uuid_in_list(uuid);
if !exists {
return Ok(());
}
list.uuids.retain(|v| {
let v: Uuid = v.into();
v != uuid
});
let bucket = bucket.read().await;
NatsMetadataStore::update_list(bucket, list).await
})
}
}
@@ -0,0 +1,202 @@
mod metadata_store;
mod notification_store;
use async_nats::ConnectOptions;
use async_nats::jetstream::Context as JetStream;
use async_nats::jetstream::kv::{Config, Store};
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::JobSchedulerError;
pub use metadata_store::NatsMetadataStore;
pub use notification_store::NatsNotificationStore;
pub fn sanitize_nats_key(key: &str) -> String {
key.replace('#', ".")
.replace(':', ".")
.replace('/', ".")
.replace('=', "_")
}
pub fn sanitize_nats_bucket(bucket: &str) -> String {
sanitize_nats_key(bucket).replace('.', "-")
}
#[derive(Clone)]
pub struct NatsStore {
pub context: Arc<RwLock<JetStream>>,
pub inited: bool,
pub bucket_name: String,
pub bucket: Arc<RwLock<Store>>,
}
impl NatsStore {
pub async fn default() -> Self {
let nats_host =
std::env::var("NATS_HOST").unwrap_or_else(|_| "nats://localhost".to_string());
let nats_app = std::env::var("NATS_APP").unwrap_or_else(|_| "Unknown Nats app".to_string());
let connection = {
let username = std::env::var("NATS_USERNAME");
let password = std::env::var("NATS_PASSWORD");
match (username, password) {
(Ok(username), Ok(password)) => {
let mut options = ConnectOptions::new()
.user_and_password(username, password)
.name(&*nats_app);
if std::path::Path::new("/etc/runtime-certs/").exists() {
options = options
.add_root_certificates("/etc/runtime-certs/ca.crt".into())
.add_client_certificate(
"/etc/runtime-certs/tls.crt".into(),
"/etc/runtime-certs/tls.key".into(),
)
}
options.connect(&*nats_host).await
}
_ => async_nats::connect(&*nats_host).await,
}
}
.unwrap();
let bucket_name =
std::env::var("NATS_BUCKET_NAME").unwrap_or_else(|_| "tokiocron".to_string());
let bucket_name = sanitize_nats_bucket(&bucket_name);
let bucket_description = std::env::var("NATS_BUCKET_DESCRIPTION")
.unwrap_or_else(|_| "Tokio Cron Scheduler".to_string());
let context = async_nats::jetstream::new(connection);
let bucket = context
.create_key_value(Config {
bucket: bucket_name.clone(),
description: bucket_description,
history: 1,
..Default::default()
})
.await
.unwrap();
let context = Arc::new(RwLock::new(context));
let bucket = Arc::new(RwLock::new(bucket));
Self {
context,
inited: true,
bucket_name,
bucket,
}
}
}
impl NatsStore {
/// Create a new builder
pub fn new_builder() -> NatsStoreBuilder {
NatsStoreBuilder::default()
}
}
#[derive(Default)]
pub struct NatsStoreBuilder {
pub username: Option<String>,
pub password: Option<String>,
pub host: Option<String>,
pub app_name: Option<String>,
pub bucket: Option<String>,
pub bucket_description: Option<String>,
}
impl NatsStoreBuilder {
pub fn username(mut self, username: String) -> Self {
self.username = Some(username);
self
}
pub fn password(mut self, password: String) -> Self {
self.password = Some(password);
self
}
pub fn host(mut self, host: String) -> Self {
self.host = Some(host);
self
}
pub fn app_name(mut self, app_name: String) -> Self {
self.app_name = Some(app_name);
self
}
pub fn bucket(mut self, bucket: String) -> Self {
self.bucket = Some(bucket);
self
}
pub fn bucket_description(mut self, bucket_description: String) -> Self {
self.bucket_description = Some(bucket_description);
self
}
/// Build a NatsStore
pub async fn build(self) -> Result<NatsStore, JobSchedulerError> {
let NatsStoreBuilder {
username,
password,
host,
app_name,
bucket,
bucket_description,
} = self;
let host = host.ok_or_else(|| JobSchedulerError::BuilderNeedsField("host".to_string()))?;
let bucket =
bucket.ok_or_else(|| JobSchedulerError::BuilderNeedsField("bucket".to_string()))?;
let bucket_name = sanitize_nats_bucket(&*bucket);
let connection = {
let options = {
let mut options = match (username, password) {
(Some(username), Some(password)) => {
Ok(ConnectOptions::new().user_and_password(username, password))
}
(None, None) => Ok(ConnectOptions::new()),
_ => Err(JobSchedulerError::BuilderNeedsField(
"username and password both be set".to_string(),
)),
}?;
if std::path::Path::new("/etc/runtime-certs/").exists() {
options = options
.add_root_certificates("/etc/runtime-certs/ca.crt".into())
.add_client_certificate(
"/etc/runtime-certs/tls.crt".into(),
"/etc/runtime-certs/tls.key".into(),
)
}
if let Some(app_name) = app_name {
options = options.name(&*app_name);
}
options
};
options.connect(&*host).await
}
.map_err(|e| JobSchedulerError::NatsCouldNotConnect(e.to_string()))?;
let mut context = async_nats::jetstream::new(connection);
let mut bucket_config = Config {
bucket: bucket_name.clone(),
history: 1,
..Default::default()
};
if let Some(description) = bucket_description {
bucket_config = Config {
description,
..bucket_config
};
}
let bucket = context
.create_key_value(bucket_config)
.await
.map_err(|e| JobSchedulerError::NatsCouldNotCreateKvStore(e.to_string()))?;
let context = Arc::new(RwLock::new(context));
let bucket = Arc::new(RwLock::new(bucket));
Ok(NatsStore {
context,
inited: true,
bucket_name,
bucket,
})
}
}
@@ -0,0 +1,453 @@
use crate::job::job_data_prost::{
JobAndNotifications, JobState, ListOfJobsAndNotifications, NotificationData,
};
use crate::job::{JobId, NotificationId};
use crate::nats::{NatsStore, sanitize_nats_key};
use crate::store::{DataStore, InitStore, NotificationStore};
use crate::{JobSchedulerError, JobUuid};
use async_nats::jetstream::kv::Store;
use bytes::Bytes;
use prost::Message;
use std::future::Future;
use std::pin::Pin;
use tokio::sync::RwLockReadGuard;
use tracing::error;
use uuid::Uuid;
const LIST_NAME: &str = "TCS_NOTIFICATION_LIST";
const NOTIFICATION_PRE: &str = "NOTIF_";
#[derive(Clone)]
pub struct NatsNotificationStore {
pub store: NatsStore,
}
fn uuid_to_nats_id(uuid: Uuid) -> String {
let uuid = NOTIFICATION_PRE.to_string() + &*uuid.to_string();
sanitize_nats_key(&*uuid)
}
impl DataStore<NotificationData> for NatsNotificationStore {
fn get(
&mut self,
id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<Option<NotificationData>, JobSchedulerError>> + Send>>
{
let bucket = self.store.bucket.clone();
Box::pin(async move {
let r = bucket.read().await;
let id = uuid_to_nats_id(id);
r.get(&*id)
.await
.map_err(|e| {
error!("Error getting data {:?}", e);
JobSchedulerError::GetJobData
})
.map(|v| v.and_then(|v| NotificationData::decode(v).ok()))
})
}
fn add_or_update(
&mut self,
data: NotificationData,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let bucket = self.store.bucket.clone();
let notification_id: Uuid = data
.job_id
.as_ref()
.and_then(|j| j.notification_id.as_ref())
.unwrap()
.into();
let job_id: Uuid = data
.job_id
.as_ref()
.and_then(|j| j.job_id.as_ref())
.unwrap()
.into();
let get = self.get(notification_id);
let add_to_list = self.add_to_list_of_guids(job_id, notification_id);
Box::pin(async move {
let bucket = bucket.read().await;
let bytes = data.encode_to_vec();
let prev = get.await;
let uuid = uuid_to_nats_id(notification_id);
let done = match prev {
Ok(Some(_)) => bucket.put(&*uuid, Bytes::from(bytes)).await.map_err(|_| ()),
Ok(None) => bucket
.create(&*uuid, Bytes::from(bytes))
.await
.map_err(|_| ()),
Err(e) => {
error!(
"Error getting existing value {:?}, assuming does not exist and hope for the best",
e
);
bucket
.create(&*uuid, Bytes::from(bytes))
.await
.map_err(|_| ())
}
};
let added = add_to_list.await;
match (done, added) {
(Ok(_), Ok(_)) => Ok(()),
_ => Err(JobSchedulerError::CantAdd),
}
})
}
fn delete(
&mut self,
guid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let bucket = self.store.bucket.clone();
let removed_from_list = self.remove_from_list(guid);
Box::pin(async move {
let bucket = bucket.read().await;
let guid = uuid_to_nats_id(guid);
let deleted = bucket.delete(&*guid).await;
let removed_from_list = removed_from_list.await;
match (deleted, removed_from_list) {
(Ok(_), Ok(_)) => Ok(()),
_ => Err(JobSchedulerError::CantRemove),
}
})
}
}
impl InitStore for NatsNotificationStore {
fn init(&mut self) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
Box::pin(async move {
// Nop
// That being said. Would've been better to do the connection startup here.
Ok(())
})
}
fn inited(&mut self) -> Pin<Box<dyn Future<Output = Result<bool, JobSchedulerError>> + Send>> {
let inited = self.store.inited;
Box::pin(async move { Ok(inited) })
}
}
impl NotificationStore for NatsNotificationStore {
fn list_notification_guids_for_job_and_state(
&mut self,
job: JobId,
state: JobState,
) -> Pin<Box<dyn Future<Output = Result<Vec<NotificationId>, JobSchedulerError>> + Send>> {
let list_of_notification_guids = self.list_notification_guids_for_job_id(job);
let bucket = self.store.bucket.clone();
let state = state as i32;
Box::pin(async move {
let list_of_notification_guids = list_of_notification_guids.await;
if let Err(e) = list_of_notification_guids {
error!("Could not get list of guids {:?}", e);
return Err(e);
}
let list_of_notification_guids = list_of_notification_guids.unwrap();
let bucket = bucket.read().await;
let mut notification_ids = vec![];
for notification_id in list_of_notification_guids {
let id = bucket
.get(&*uuid_to_nats_id(notification_id))
.await
.ok()
.flatten()
.and_then(|b| NotificationData::decode(b).ok())
.filter(|nd| nd.job_states.contains(&state))
.map(|_| notification_id);
if let Some(id) = id {
notification_ids.push(id);
}
}
Ok(notification_ids)
})
}
fn list_notification_guids_for_job_id(
&mut self,
job_id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<Vec<Uuid>, JobSchedulerError>> + Send>> {
let list_guids = self.list_guids();
Box::pin(async move {
let list_guids = list_guids.await;
if let Err(e) = list_guids {
error!("Error getting {:?}", e);
return Err(e);
}
let list_guids = list_guids.unwrap();
let list = list_guids
.job_and_notifications
.iter()
.flat_map(|j| {
j.job_id
.as_ref()
.filter(|id| {
let id: Uuid = JobUuid {
id1: id.id1,
id2: id.id2,
}
.into();
id == job_id
})
.map(|_i| {
j.notification_ids
.iter()
.map(|n| {
let n: Uuid = n.into();
n
})
.collect::<Vec<_>>()
})
.unwrap_or_default()
})
.collect::<Vec<_>>();
Ok(list)
})
}
fn delete_notification_for_state(
&mut self,
notification_id: Uuid,
state: JobState,
) -> Pin<Box<dyn Future<Output = Result<bool, JobSchedulerError>> + Send>> {
let get = self.get(notification_id);
let mut self_clone = self.clone();
Box::pin(async move {
let data = get.await;
let mut data = match data {
Ok(Some(get)) => get,
Ok(None) => {
error!("Notification not found {:?}", notification_id);
return Err(JobSchedulerError::CantRemove);
}
Err(e) => {
error!("Error getting notification {:?}", e);
return Err(e);
}
};
let state = state as i32;
let mut deleted = false;
data.job_states.retain(|s| {
let ret = *s != state;
deleted |= !ret;
ret
});
if data.job_states.is_empty() {
// Need to delete
let delete = self_clone.delete(notification_id).await;
if let Err(e) = delete {
error!("Could not delete notification {:?}", e);
return Err(e);
}
let delete = self_clone.remove_from_list(notification_id).await;
delete.map(|_| true)
} else {
// Need to update
self_clone.add_or_update(data).await.map(|_| deleted)
}
})
}
fn delete_for_job(
&mut self,
job_id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let list_guids = self.list_guids();
let mut self_clone = self.clone();
Box::pin(async move {
let list_guids = list_guids.await;
if let Err(e) = list_guids {
error!("Error getting list of guids {:?}", e);
return Err(e);
}
let list_guids = list_guids.unwrap();
let notifications = list_guids
.job_and_notifications
.into_iter()
.filter(|l| {
l.job_id
.as_ref()
.map(|u| {
let u: Uuid = u.into();
u == job_id
})
.is_some()
})
.flat_map(|l: JobAndNotifications| l.notification_ids)
.map(|u| {
let u: Uuid = u.into();
u
});
for notification_id in notifications {
let deleted = self_clone.delete(notification_id).await;
if let Err(e) = deleted {
error!("Error deleting notification {:?}", notification_id);
return Err(e);
}
}
Ok(())
})
}
}
impl NatsNotificationStore {
pub async fn default() -> Self {
let store = NatsStore::default().await;
Self { store }
}
fn list_guids(
&self,
) -> Pin<Box<dyn Future<Output = Result<ListOfJobsAndNotifications, JobSchedulerError>> + Send>>
{
let bucket = self.store.bucket.clone();
Box::pin(async move {
let r = bucket.read().await;
let list = r.get(&*sanitize_nats_key(LIST_NAME)).await;
match list {
Ok(Some(list)) => ListOfJobsAndNotifications::decode(list).map_err(|e| {
error!("Error decoding list value {:?}", e);
JobSchedulerError::CantListGuids
}),
Ok(None) => Ok(ListOfJobsAndNotifications::default()),
Err(e) => {
error!("Error getting list of guids {:?}", e);
Err(JobSchedulerError::CantListGuids)
}
}
})
}
fn add_to_list_of_guids(
&self,
job_id: JobId,
notification_id: NotificationId,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let list = self.list_guids();
let bucket = self.store.bucket.clone();
Box::pin(async move {
let list = list.await;
if let Err(e) = list {
error!("Could not get list of guids {:?}", e);
return Err(JobSchedulerError::ErrorLoadingGuidList);
}
let mut list = list.unwrap();
let mut job_found = false;
for job in list.job_and_notifications.iter_mut() {
let this_job = job
.job_id
.as_ref()
.filter(|j| {
let j: Uuid = JobUuid {
id1: j.id1,
id2: j.id2,
}
.into();
j == job_id
})
.is_some();
if this_job {
job_found = true;
let contains = job.notification_ids.iter().any(|u| {
let u: Uuid = JobUuid {
id1: u.id1,
id2: u.id2,
}
.into();
u == notification_id
});
if !contains {
let notification: JobUuid = notification_id.into();
job.notification_ids.push(notification)
}
}
}
if !job_found {
let job_id: JobUuid = job_id.into();
let notification_id: JobUuid = notification_id.into();
list.job_and_notifications.push(JobAndNotifications {
job_id: Some(job_id),
notification_ids: vec![notification_id],
});
}
let bucket = bucket.read().await;
NatsNotificationStore::update_list(bucket, list).await
})
}
async fn update_list(
bucket: RwLockReadGuard<'_, Store>,
list: ListOfJobsAndNotifications,
) -> Result<(), JobSchedulerError> {
let has_list_already = bucket
.get(&*sanitize_nats_key(LIST_NAME))
.await
.ok()
.flatten()
.is_some();
if has_list_already {
bucket
.put(
&*sanitize_nats_key(LIST_NAME),
Bytes::from(list.encode_to_vec()),
)
.await
.map_err(|e| {
error!("Error saving list of guids {:?}", e);
JobSchedulerError::CantAdd
})
} else {
bucket
.create(
&*sanitize_nats_key(LIST_NAME),
Bytes::from(list.encode_to_vec()),
)
.await
.map_err(|e| {
error!("Error saving list of guids {:?}", e);
JobSchedulerError::CantAdd
})
}
.map(|_| ())
}
fn remove_from_list(
&self,
uuid: NotificationId,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let list = self.list_guids();
let bucket = self.store.bucket.clone();
Box::pin(async move {
let list = list.await;
if let Err(e) = list {
error!("Could not get list of guids {:?}", e);
return Err(JobSchedulerError::ErrorLoadingGuidList);
}
let mut list = list.unwrap();
let mut exists = false;
for job_and_notifications in list.job_and_notifications.iter_mut() {
job_and_notifications.notification_ids.retain(|n| {
let n: Uuid = n.into();
let retain = n != uuid;
exists |= !retain;
retain
});
}
if !exists {
return Ok(());
}
let bucket = bucket.read().await;
NatsNotificationStore::update_list(bucket, list).await
})
}
}
@@ -0,0 +1,162 @@
use crate::context::Context;
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::{JobState, NotificationData};
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::{JobState, NotificationData};
use crate::store::NotificationStore;
use crate::{JobSchedulerError, OnJobNotification};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::broadcast::{Receiver, Sender};
use tracing::{error, warn};
use uuid::Uuid;
#[derive(Default)]
pub struct NotificationCreator {}
impl NotificationCreator {
async fn listen_for_additions(
storage: Arc<RwLock<Box<dyn NotificationStore + Send + Sync>>>,
mut rx: Receiver<(NotificationData, Arc<RwLock<Box<OnJobNotification>>>)>,
tx_created: Sender<Result<Uuid, (JobSchedulerError, Option<Uuid>)>>,
) {
loop {
let val = rx.recv().await;
if let Err(e) = val {
error!("Error receiving {:?}", e);
break;
}
let (data, _) = val.unwrap();
if data.job_id.is_none() {
error!("Empty job id {:?}", data);
continue;
}
let notification_id = data
.job_id
.as_ref()
.and_then(|j| j.notification_id.as_ref());
if notification_id.is_none() {
error!("Empty job id or notification id {:?}", data);
continue;
}
let notification_id: Uuid = notification_id.unwrap().into();
let mut storage = storage.write().await;
let val = storage.get(notification_id).await;
let val = match val {
Ok(Some(mut val)) => {
for state in data.job_states {
if !val.job_states.contains(&state) {
val.job_states.push(state);
}
}
val
}
_ => data,
};
let val = storage.add_or_update(val).await;
if let Err(e) = val {
error!("Error adding or updating {:?}", e);
if let Err(e) = tx_created.send(Err((e, Some(notification_id)))) {
error!("Error sending adding or updating error {:?}", e);
}
continue;
}
if let Err(e) = tx_created.send(Ok(notification_id)) {
warn!("Error sending created state {:?}", e);
}
}
}
pub fn init(
&mut self,
context: &Context,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let rx = context.notify_create_tx.subscribe();
let tx_created = context.notify_created_tx.clone();
let storage = context.notification_storage.clone();
Box::pin(async move {
tokio::spawn(NotificationCreator::listen_for_additions(
storage, rx, tx_created,
));
Ok(())
})
}
pub async fn add(
context: &Context,
run: Box<OnJobNotification>,
job_states: Vec<JobState>,
job_id: &Uuid,
) -> Result<Uuid, JobSchedulerError> {
let notification_id = Uuid::new_v4();
let data = NotificationData {
#[cfg(feature = "has_bytes")]
job_id: Some(crate::job::job_data_prost::JobIdAndNotification {
job_id: Some(job_id.into()),
notification_id: Some(notification_id.into()),
}),
#[cfg(not(feature = "has_bytes"))]
job_id: Some(crate::job::job_data::JobIdAndNotification {
job_id: Some(job_id.into()),
notification_id: Some(notification_id.into()),
}),
job_states: job_states.iter().map(|i| *i as i32).collect::<Vec<_>>(),
extra: vec![],
};
let create_tx = context.notify_create_tx.clone();
let mut created_rx = context.notify_created_tx.subscribe();
let (tx, rx) = std::sync::mpsc::channel();
tokio::spawn(async move {
tokio::spawn(async move {
// TODO can maybe not use RwLock
if let Err(_e) = create_tx.send((data, Arc::new(RwLock::new(run)))) {
error!("Error sending notification data");
}
});
'receiving_additions: loop {
let created = created_rx.recv().await;
match created {
Ok(e) => match e {
Ok(uuid) => {
if uuid == notification_id {
if let Err(e) = tx.send(Ok(uuid)) {
error!("Error sending notification addition success {:?}", e);
}
break 'receiving_additions;
}
}
Err((e, Some(uuid))) => {
if uuid == notification_id {
if let Err(e) = tx.send(Err(e)) {
error!("Error sending notification addition failure {:?}", e);
}
break 'receiving_additions;
}
}
_ => {}
},
Err(e) => {
error!("Error receiving from created {:?}", e);
}
}
}
});
let rx = rx.recv();
match rx {
Ok(ret) => ret,
Err(e) => {
error!("Error receiving status from notification addition {:?}", e);
Err(JobSchedulerError::CantAdd)
}
}
}
}
@@ -0,0 +1,164 @@
use crate::JobSchedulerError;
use crate::context::{Context, NotificationDeletedResult};
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::JobState;
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::JobState;
use crate::job::{JobId, NotificationId};
use crate::store::NotificationStore;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::broadcast::{Receiver, Sender};
use tracing::error;
#[derive(Default)]
pub struct NotificationDeleter {}
impl NotificationDeleter {
async fn listen_to_job_removals(
storage: Arc<RwLock<Box<dyn NotificationStore + Send + Sync>>>,
mut rx_job_delete: Receiver<JobId>,
tx_notification_deleted: Sender<NotificationDeletedResult>,
) {
loop {
let val = rx_job_delete.recv().await;
if let Err(e) = val {
error!("Error receiving delete jobs {:?}", e);
break;
}
let job_id = val.unwrap();
let mut storage = storage.write().await;
let guids = storage.list_notification_guids_for_job_id(job_id).await;
if let Err(e) = guids {
error!("Error with getting guids for job id {:?}", e);
continue;
}
let guids = guids.unwrap();
// TODO first check for removal callback
for notification_id in guids {
if let Err(e) = storage.delete(notification_id).await {
error!("Error deleting notification {:?}", e);
continue;
}
if let Err(e) = tx_notification_deleted.send(Ok((notification_id, true, None))) {
error!("Error sending deletion {:?}", e);
continue;
}
}
}
}
async fn listen_for_notification_removals(
storage: Arc<RwLock<Box<dyn NotificationStore + Send + Sync>>>,
mut rx: Receiver<(NotificationId, Option<Vec<JobState>>)>,
tx_deleted: Sender<NotificationDeletedResult>,
) {
loop {
let val = rx.recv().await;
if let Err(e) = val {
error!("Error receiving notification removals {:?}", e);
break;
}
let (uuid, states) = val.unwrap();
{
let mut storage = storage.write().await;
if let Some(states) = states {
for state in states {
let delete = storage.delete_notification_for_state(uuid, state).await;
if let Err(e) = delete {
error!("Error deleting notification for state {:?}", e);
continue;
}
let delete = delete.unwrap();
if let Err(e) = tx_deleted.send(Ok((uuid, delete, Some(vec![state])))) {
error!("Error sending notification deleted state {:?}", e);
}
}
} else {
let w = storage.delete(uuid).await;
if let Err(e) = w {
error!("Error deleting notification for all states {:?}", e);
continue;
}
if let Err(e) = tx_deleted.send(Ok((uuid, true, None))) {
error!("Error sending {:?}", e);
}
}
}
}
}
pub fn init(
&mut self,
context: &Context,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let rx_job_delete = context.job_delete_tx.subscribe();
let rx_notification_delete = context.notify_delete_tx.subscribe();
let tx_notification_deleted = context.notify_deleted_tx.clone();
let storage = context.notification_storage.clone();
Box::pin(async move {
tokio::spawn(NotificationDeleter::listen_to_job_removals(
storage.clone(),
rx_job_delete,
tx_notification_deleted.clone(),
));
tokio::spawn(NotificationDeleter::listen_for_notification_removals(
storage,
rx_notification_delete,
tx_notification_deleted,
));
Ok(())
})
}
pub fn remove(
context: &Context,
notification_id: &NotificationId,
states: Option<Vec<JobState>>,
) -> Result<(NotificationId, bool), JobSchedulerError> {
let notification_id = *notification_id;
let delete_tx = context.notify_delete_tx.clone();
let mut deleted_rx = context.notify_deleted_tx.subscribe();
let (tx, rx) = std::sync::mpsc::channel();
tokio::spawn(async move {
tokio::spawn(async move {
if let Err(e) = delete_tx.send((notification_id, states)) {
error!("Error sending notification removal {:?}", e);
}
});
while let Ok(val) = deleted_rx.recv().await {
match val {
Ok((uuid, deleted, _)) => {
if uuid == notification_id {
if let Err(e) = tx.send(Ok((uuid, deleted))) {
error!("Error sending notification removal success {:?}", e);
}
break;
}
}
Err((e, Some(uuid))) => {
if uuid == notification_id {
if let Err(e) = tx.send(Err(e)) {
error!("Error sending removal error {:?}", e);
}
break;
}
}
_ => {}
}
}
});
let ret = rx.recv();
match ret {
Ok(ret) => ret,
Err(e) => {
error!("Error getting result from notification removal {:?}", e);
Err(JobSchedulerError::CantRemove)
}
}
}
}
@@ -0,0 +1,29 @@
mod creator;
mod deleter;
mod runner;
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::NotificationData;
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::NotificationData;
use crate::job::{JobId, NotificationId};
pub use creator::NotificationCreator;
pub use deleter::NotificationDeleter;
pub use runner::NotificationRunner;
use uuid::Uuid;
impl NotificationData {
pub fn job_id_and_notification_id_from_data(&self) -> Option<(JobId, NotificationId)> {
match self.job_id.as_ref() {
Some(j) => match (j.job_id.as_ref(), j.notification_id.as_ref()) {
(Some(job_id), Some(notification_id)) => {
let job_id: Uuid = job_id.into();
let notification_id: Uuid = notification_id.into();
Some((job_id, notification_id))
}
_ => None,
},
None => None,
}
}
}
@@ -0,0 +1,87 @@
use crate::JobSchedulerError;
use crate::context::Context;
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::JobState;
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::JobState;
use crate::job::to_code::NotificationCode;
use crate::store::NotificationStore;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::broadcast::Receiver;
use tokio::sync::broadcast::error::RecvError;
use tracing::error;
use uuid::Uuid;
#[derive(Default)]
pub struct NotificationRunner {}
impl NotificationRunner {
async fn listen_for_activations(
code: Arc<RwLock<Box<dyn NotificationCode + Send + Sync>>>,
mut rx: Receiver<(Uuid, JobState)>,
storage: Arc<RwLock<Box<dyn NotificationStore + Send + Sync>>>,
) {
loop {
let val = rx.recv().await;
if let Err(e) = val {
error!("Error receiving value {:?}", e);
if matches!(e, RecvError::Closed) {
break;
}
continue;
}
let (job_id, state) = val.unwrap();
let mut storage = storage.write().await;
let notifications = storage
.list_notification_guids_for_job_and_state(job_id, state)
.await;
if let Err(_e) = notifications {
error!(
"Error getting the list of notifications guids for job {:?} and state {:?}",
job_id, state
);
continue;
}
let notifications = notifications.unwrap();
let mut code = code.write().await;
for notification_id in notifications {
let code = code.get(notification_id).await;
match code {
Ok(Some(code)) => {
let code = code.clone();
tokio::spawn(async move {
let mut code = code.write().await;
(code)(job_id, notification_id, state).await;
});
}
_ => {
error!(
" nCould not get notification code for {:?}",
notification_id
);
continue;
}
}
}
}
}
pub fn init(
&mut self,
context: &Context,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let code = context.notification_code.clone();
let rx = context.notify_tx.subscribe();
let storage = context.notification_storage.clone();
Box::pin(async move {
tokio::spawn(NotificationRunner::listen_for_activations(
code, rx, storage,
));
Ok(())
})
}
}
@@ -0,0 +1,453 @@
use crate::job::job_data_prost::{CronJob, JobType, NonCronJob};
use crate::postgres::PostgresStore;
use crate::store::{DataStore, InitStore, MetaDataStorage};
use crate::{JobAndNextTick, JobSchedulerError, JobStoredData, JobUuid};
use chrono::{DateTime, Utc};
use std::convert::TryFrom;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio_postgres::Row;
use tracing::error;
use uuid::Uuid;
const TABLE: &str = "job";
#[derive(Clone)]
pub struct PostgresMetadataStore {
pub store: Arc<RwLock<PostgresStore>>,
pub init_tables: bool,
pub table: String,
}
impl Default for PostgresMetadataStore {
fn default() -> Self {
let init_tables = std::env::var("POSTGRES_INIT_METADATA")
.map(|s| s.to_lowercase() == "true")
.unwrap_or_default();
let table =
std::env::var("POSTGRES_METADATA_TABLE").unwrap_or_else(|_| TABLE.to_lowercase());
let store = Arc::new(RwLock::new(PostgresStore::default()));
Self {
init_tables,
table,
store,
}
}
}
impl DataStore<JobStoredData> for PostgresMetadataStore {
fn get(
&mut self,
id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<Option<JobStoredData>, JobSchedulerError>> + Send>>
{
let store = self.store.clone();
let table = self.table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::GetJobData),
PostgresStore::Inited(store) => {
let store = store.read().await;
let sql = "select \
id, last_updated, next_tick, last_tick, job_type, count, \
ran, stopped, schedule, repeating, repeated_every, \
extra, time_offset_seconds \
from "
.to_string()
+ &*table
+ " where id = $1 limit 1";
let row = store.query_one(&*sql, &[&id]).await;
if let Err(e) = row {
error!("Error getting value {:?}", e);
return Err(JobSchedulerError::GetJobData);
}
let row = row.unwrap();
Ok(Some(row.into()))
}
}
})
}
fn add_or_update(
&mut self,
data: JobStoredData,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let store = self.store.clone();
let table = self.table.clone();
Box::pin(async move {
use crate::job::job_data_prost::job_stored_data::Job::CronJob as CronJobType;
use crate::job::job_data_prost::job_stored_data::Job::NonCronJob as NonCronJobType;
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::UpdateJobData),
PostgresStore::Inited(store) => {
let uuid: Uuid = data.id.as_ref().unwrap().into();
let store = store.read().await;
let sql = "INSERT INTO ".to_string()
+ &*table
+ " (\
id, last_updated, next_tick, job_type, count, \
ran, stopped, schedule, repeating, repeated_every, \
extra, last_tick, time_offset_seconds \
)\
VALUES (\
$1, $2, $3, $4, $5, \
$6, $7, $8, $9, $10,\
$11, $12, $13 \
)\
ON CONFLICT (id) \
DO \
UPDATE \
SET \
last_updated=$2, next_tick=$3, job_type=$4, count=$5, \
ran=$6, stopped=$7, schedule=$8, repeating=$9, repeated_every=$10, \
extra=$11, last_tick=$12, time_offset_seconds=$13
";
let last_updated = data.last_updated.as_ref().map(|i| *i as i64);
let next_tick = data.next_tick as i64;
let job_type = data.job_type;
let count = data.count as i32;
let ran = data.ran;
let stopped = data.stopped;
let schedule = match data.job.as_ref() {
Some(CronJobType(ct)) => Some(ct.schedule.clone()),
_ => None,
};
let repeating = match data.job.as_ref() {
Some(NonCronJobType(ct)) => Some(ct.repeating),
_ => None,
};
let repeated_every = match data.job.as_ref() {
Some(NonCronJobType(ct)) => Some(ct.repeated_every as i64),
_ => None,
};
let extra = data.extra;
let last_tick = data.last_tick.as_ref().map(|i| *i as i64);
let time_offset_seconds = data.time_offset_seconds;
let val = store
.query(
&*sql,
&[
&uuid,
&last_updated,
&next_tick,
&job_type,
&count,
&ran,
&stopped,
&schedule,
&repeating,
&repeated_every,
&extra,
&last_tick,
&time_offset_seconds,
],
)
.await;
if let Err(e) = val {
error!("Error {:?}", e);
Err(JobSchedulerError::CantAdd)
} else {
Ok(())
}
}
}
})
}
fn delete(
&mut self,
guid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let store = self.store.clone();
let table = self.table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::CantRemove),
PostgresStore::Inited(store) => {
let store = store.read().await;
let sql = "DELETE FROM ".to_string() + &*table + " WHERE id = $1";
let val = store.query(&*sql, &[&guid]).await;
match val {
Ok(_) => Ok(()),
Err(e) => {
error!("Error deleting job data {:?}", e);
Err(JobSchedulerError::CantRemove)
}
}
}
}
})
}
}
impl From<Row> for JobStoredData {
fn from(row: Row) -> Self {
/*
id, last_updated, next_tick, last_tick, job_type, count, \
ran, stopped, schedule, repeating, repeated_every, \
extra, time_offset_seconds
*/
let id: Uuid = row.get(0);
let last_updated = row.try_get(1).ok().map(|i: i64| i as u64);
let next_tick = row
.try_get(2)
.ok()
.map(|i: i64| i as u64)
.unwrap_or_default();
let last_tick = row.try_get(3).ok().map(|i: i64| i as u64);
let job_type: i32 = row.try_get(4).unwrap_or_default();
let count = row.try_get(5).unwrap_or_default();
let ran = row.try_get(6).unwrap_or_default();
let stopped = row.try_get(7).unwrap_or_default();
let job = {
use crate::job::job_data_prost::job_stored_data::Job::CronJob as CronJobType;
use crate::job::job_data_prost::job_stored_data::Job::NonCronJob as NonCronJobType;
let job_type = JobType::try_from(job_type).ok();
match job_type {
Some(JobType::Cron) => match row.try_get(8) {
Ok(schedule) => Some(CronJobType(CronJob { schedule })),
_ => None,
},
Some(_) => {
let repeating = row.get(9);
let repeated_every = row
.try_get(10)
.ok()
.map(|i: i64| i as u64)
.unwrap_or_default();
Some(NonCronJobType(NonCronJob {
repeating,
repeated_every,
}))
}
None => None,
}
};
let extra = row.try_get(11).unwrap_or_default();
let time_offset_seconds = row.try_get(12).unwrap_or_default();
Self {
id: Some(id.into()),
last_updated,
last_tick,
next_tick,
job_type,
count,
extra,
ran,
stopped,
job,
time_offset_seconds,
}
}
}
impl InitStore for PostgresMetadataStore {
fn init(&mut self) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let inited = self.inited();
let store = self.store.clone();
let init_tables = self.init_tables;
let table = self.table.clone();
Box::pin(async move {
let inited = inited.await;
if matches!(inited, Ok(false)) || matches!(inited, Err(_)) {
let mut w = store.write().await;
let val = w.clone();
let val = val.init().await;
match val {
Ok(v) => {
if init_tables {
if let PostgresStore::Inited(client) = &v {
let v = client.read().await;
let sql = "CREATE TABLE IF NOT EXISTS ".to_string()
+ &*table
+ " (\
id UUID,\
last_updated BIGINT,\
next_tick BIGINT,\
last_tick BIGINT,\
job_type INTEGER NOT NULL,\
count INTEGER,\
ran BOOL,\
stopped BOOL,\
schedule TEXT,\
repeating BOOL,\
repeated_every BIGINT,\
time_offset_seconds INTEGER, \
extra BYTEA, \
CONSTRAINT pk_metadata PRIMARY KEY (id) \
)";
let create = v.execute(&*sql, &[]).await;
if let Err(e) = create {
error!("Error on init Postgres Metadata store {:?}", e);
return Err(JobSchedulerError::CantInit);
}
}
}
*w = v;
Ok(())
}
Err(e) => {
error!("Error initialising {:?}", e);
Err(e)
}
}
} else {
Ok(())
}
})
}
fn inited(&mut self) -> Pin<Box<dyn Future<Output = Result<bool, JobSchedulerError>> + Send>> {
let store = self.store.clone();
Box::pin(async move {
let store = store.read().await;
Ok(store.inited())
})
}
}
impl MetaDataStorage for PostgresMetadataStore {
fn list_next_ticks(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<Vec<JobAndNextTick>, JobSchedulerError>> + Send>> {
let store = self.store.clone();
let table = self.table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::CantListNextTicks),
PostgresStore::Inited(store) => {
let store = store.read().await;
let now = Utc::now().timestamp();
let sql = "SELECT \
id, job_type, next_tick, last_tick \
FROM "
.to_string()
+ &*table
+ " \
WHERE \
next_tick > 0 \
AND next_tick < $1";
let rows = store.query(&*sql, &[&now]).await;
match rows {
Ok(rows) => Ok(rows
.iter()
.map(|row| {
let id: Uuid = row.get(0);
let id: JobUuid = id.into();
let job_type = row.get(1);
let next_tick = row
.try_get(2)
.ok()
.map(|i: i64| i as u64)
.unwrap_or_default();
let last_tick = row.try_get(3).ok().map(|i: i64| i as u64);
JobAndNextTick {
id: Some(id),
job_type,
next_tick,
last_tick,
}
})
.collect::<Vec<_>>()),
Err(e) => {
error!("Error getting next ticks {:?}", e);
Err(JobSchedulerError::CantListNextTicks)
}
}
}
}
})
}
fn set_next_and_last_tick(
&mut self,
guid: Uuid,
next_tick: Option<DateTime<Utc>>,
last_tick: Option<DateTime<Utc>>,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let store = self.store.clone();
let table = self.table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::UpdateJobData),
PostgresStore::Inited(store) => {
let store = store.read().await;
let next_tick = next_tick.map(|b| b.timestamp()).unwrap_or(0);
let last_tick = last_tick.map(|b| b.timestamp());
let sql = "UPDATE ".to_string()
+ &*table
+ " \
SET \
next_tick=$1, last_tick=$2 \
WHERE \
id = $3";
let resp = store.query(&sql, &[&next_tick, &last_tick, &guid]).await;
if let Err(e) = resp {
error!("Error updating next and last tick {:?}", e);
Err(JobSchedulerError::UpdateJobData)
} else {
Ok(())
}
}
}
})
}
fn time_till_next_job(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<Option<Duration>, JobSchedulerError>> + Send>> {
let store = self.store.clone();
let table = self.table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::CouldNotGetTimeUntilNextTick),
PostgresStore::Inited(store) => {
let store = store.read().await;
let now = Utc::now().timestamp();
let sql = "SELECT \
next_tick \
FROM "
.to_string()
+ &*table
+ " \
WHERE \
next_tick > 0 \
AND next_tick > $1 \
ORDER BY next_tick ASC \
LIMIT 1";
let row = store.query(&*sql, &[&now]).await;
if let Err(e) = row {
error!("Error getting time until next job {:?}", e);
return Err(JobSchedulerError::CouldNotGetTimeUntilNextTick);
}
let row = row.unwrap();
Ok(row
.get(0)
.map(|r| r.get::<_, i64>(0))
.map(|ts| ts - now)
.filter(|ts| *ts > 0)
.map(|ts| ts as u64)
.map(std::time::Duration::from_secs))
}
}
})
}
}
@@ -0,0 +1,104 @@
mod metadata_store;
mod notification_store;
use crate::JobSchedulerError;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio_postgres::{Client, NoTls};
use tracing::error;
pub use metadata_store::PostgresMetadataStore;
pub use notification_store::PostgresNotificationStore;
#[derive(Clone)]
pub enum PostgresStore {
Created(String),
Inited(Arc<RwLock<Client>>),
}
impl PostgresStore {
pub fn inited(&self) -> bool {
matches!(self, PostgresStore::Inited(_))
}
}
impl Default for PostgresStore {
fn default() -> Self {
let url = std::env::var("POSTGRES_URL")
.map(Some)
.unwrap_or_default()
.unwrap_or_else(|| {
let db_host =
std::env::var("POSTGRES_HOST").unwrap_or_else(|_| "localhost".to_string());
let port = std::env::var("POSTGRES_PORT").unwrap_or_else(|_| "5432".to_string());
let dbname =
std::env::var("POSTGRES_DB").unwrap_or_else(|_| "postgres".to_string());
let username =
std::env::var("POSTGRES_USERNAME").unwrap_or_else(|_| "postgres".to_string());
let password = std::env::var("POSTGRES_PASSWORD")
.map(Some)
.unwrap_or_default();
let application_name = std::env::var("POSTGRES_APP_NAME")
.map(Some)
.unwrap_or_default();
"".to_string()
+ "host="
+ &*db_host
+ " port="
+ &*port
+ " dbname="
+ &*dbname
+ " user="
+ &*username
+ &*match password {
Some(password) => " password=".to_string() + &*password,
None => "".to_string(),
}
+ &*match application_name {
Some(application_name) => {
" application_name=".to_string() + &*application_name
}
None => "".to_string(),
}
});
Self::Created(url)
}
}
impl PostgresStore {
pub fn init(
self,
) -> Pin<Box<dyn Future<Output = Result<PostgresStore, JobSchedulerError>> + Send>> {
Box::pin(async move {
match self {
PostgresStore::Created(url) => {
#[cfg(feature = "postgres-openssl")]
let tls = postgres_openssl::TlsConnector;
#[cfg(feature = "postgres-native-tls")]
let tls = postgres_native_tls::TlsConnector;
#[cfg(not(any(
feature = "postgres-native-tls",
feature = "postgres-openssl"
)))]
let tls = NoTls;
let connect = tokio_postgres::connect(&*url, tls).await;
if let Err(e) = connect {
error!("Error connecting to postgres {:?}", e);
return Err(JobSchedulerError::CantInit);
}
let (client, connection) = connect.unwrap();
tokio::spawn(async move {
if let Err(e) = connection.await {
error!("Error with Postgres Connection {:?}", e);
}
});
Ok(PostgresStore::Inited(Arc::new(RwLock::new(client))))
}
PostgresStore::Inited(client) => Ok(PostgresStore::Inited(client)),
}
})
}
}
@@ -0,0 +1,416 @@
use crate::job::job_data_prost::{JobIdAndNotification, JobState, NotificationData};
use crate::job::{JobId, NotificationId};
use crate::store::{DataStore, InitStore, NotificationStore};
use crate::{JobSchedulerError, PostgresStore};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::error;
use uuid::Uuid;
const MAIN_TABLE: &str = "notification";
const STATES_TABLE: &str = "notification_state";
#[derive(Clone)]
pub struct PostgresNotificationStore {
pub store: Arc<RwLock<PostgresStore>>,
pub init_tables: bool,
pub table: String,
pub states_table: String,
}
impl Default for PostgresNotificationStore {
fn default() -> Self {
let init_tables = std::env::var("POSTGRES_INIT_NOTIFICATIONS")
.map(|s| s.to_lowercase() == "true")
.unwrap_or_default();
let table = std::env::var("POSTGRES_NOTIFICATION_TABLE")
.unwrap_or_else(|_| MAIN_TABLE.to_lowercase());
let states_table = std::env::var("POSTGRES_NOTIFICATION_STATES_TABLE")
.unwrap_or_else(|_| STATES_TABLE.to_lowercase());
let store = Arc::new(RwLock::new(PostgresStore::default()));
Self {
init_tables,
table,
states_table,
store,
}
}
}
impl DataStore<NotificationData> for PostgresNotificationStore {
fn get(
&mut self,
id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<Option<NotificationData>, JobSchedulerError>> + Send>>
{
let store = self.store.clone();
let table = self.table.clone();
let states_table = self.states_table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::GetJobData),
PostgresStore::Inited(store) => {
let store = store.read().await;
let sql =
"SELECT id, job_id, extra from ".to_string() + &*table + " where id = $1";
let row = store.query(&*sql, &[&id]).await;
if let Err(e) = row {
error!("Error fetching notification data {:?}", e);
return Err(JobSchedulerError::GetJobData);
}
let row = row.unwrap();
let row = row.get(0);
if matches!(row, None) {
return Ok(None);
}
let row = row.unwrap();
let notification_id: Uuid = row.get(0);
let job_states = {
let sql =
"SELECT state from ".to_string() + &*states_table + " where id = $1";
let row = store.query(&*sql, &[&notification_id]).await;
match row {
Ok(rows) => rows
.iter()
.map(|row| {
let val: i32 = row.get(0);
val
})
.collect::<Vec<_>>(),
Err(e) => {
error!("Error getting states {:?}", e);
vec![]
}
}
};
let job_id: Uuid = row.get(1);
let job_id = JobIdAndNotification {
job_id: Some(job_id.into()),
notification_id: Some(notification_id.into()),
};
let extra = row.get(2);
let job_id = Some(job_id);
Ok(Some(NotificationData {
job_id,
job_states,
extra,
}))
}
}
})
}
fn add_or_update(
&mut self,
data: NotificationData,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let store = self.store.clone();
let table = self.table.clone();
let states_table = self.states_table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::UpdateJobData),
PostgresStore::Inited(store) => {
let store = store.read().await;
let (job_id, notification_id) =
match data.job_id_and_notification_id_from_data() {
Some((job_id, notification_id)) => (job_id, notification_id),
None => return Err(JobSchedulerError::UpdateJobData),
};
let sql = "DELETE FROM ".to_string() + &*states_table + " WHERE id = $1";
let result = store.query(&*sql, &[&notification_id]).await;
if let Err(e) = result {
error!("Error deleting {:?}", e);
}
let sql = "INSERT INTO ".to_string()
+ &*table
+ " (id, job_id, extra) \
VALUES ($1, $2, $3) \
ON CONFLICT (id) \
DO \
UPDATE \
SET \
job_id = $2, extra = $3";
let extra = data.extra;
let result = store
.query(&*sql, &[&notification_id, &job_id, &extra])
.await;
if let Err(e) = result {
error!("Error doing the upsert {:?}", e);
}
if !data.job_states.is_empty() {
let sql = "INSERT INTO ".to_string()
+ &*states_table
+ " (id, state) VALUES "
+ &*data
.job_states
.iter()
.map(|s| format!("($1, {})", s))
.collect::<Vec<_>>()
.join(",");
let result = store.query(&sql, &[&notification_id]).await;
if let Err(e) = result {
error!("Error inserting state vals {:?}", e);
}
}
Ok(())
}
}
})
}
fn delete(
&mut self,
guid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let store = self.store.clone();
let table = self.table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::CantRemove),
PostgresStore::Inited(store) => {
let store = store.read().await;
let sql = "DELETE FROM ".to_string() + &*table + " WHERE id = $1";
store.query(&*sql, &[&guid]).await.map(|_| ()).map_err(|e| {
error!("Error deleting notification {:?}", e);
JobSchedulerError::CantRemove
})
}
}
})
}
}
impl InitStore for PostgresNotificationStore {
fn init(&mut self) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let inited = self.inited();
let store = self.store.clone();
let init_tables = self.init_tables;
let table = self.table.clone();
let states_table = self.states_table.clone();
Box::pin(async move {
let inited = inited.await;
if matches!(inited, Ok(false)) || matches!(inited, Err(_)) {
let mut w = store.write().await;
let val = w.clone();
let val = val.init().await;
match val {
Ok(v) => {
if init_tables {
if let PostgresStore::Inited(client) = &v {
let v = client.read().await;
let sql = "CREATE TABLE IF NOT EXISTS ".to_string()
+ &*table
+ " ( \
id UUID, \
job_id UUID, \
extra BYTEA, \
CONSTRAINT pk_notification_id PRIMARY KEY (id)
)";
let create = v.query(&*sql, &[]).await;
if let Err(e) = create {
error!("Error creating notification table {:?}", e);
return Err(JobSchedulerError::CantInit);
}
let sql = "CREATE TABLE IF NOT EXISTS ".to_string()
+ &*states_table
+ " (\
id UUID NOT NULL,
state INTEGER NOT NULL,
CONSTRAINT pk_notification_states PRIMARY KEY (id, state),
CONSTRAINT fk_notification_id FOREIGN KEY (id) REFERENCES "
+ &*table
+ " (id) ON DELETE CASCADE
)";
let create = v.query(&*sql, &[]).await;
if let Err(e) = create {
error!("Error creating notification states table {:?}", e);
return Err(JobSchedulerError::CantInit);
}
}
}
*w = v;
Ok(())
}
Err(e) => {
error!("Error initialising {:?}", e);
Err(e)
}
}
} else {
Ok(())
}
})
}
fn inited(&mut self) -> Pin<Box<dyn Future<Output = Result<bool, JobSchedulerError>> + Send>> {
let store = self.store.clone();
Box::pin(async move {
let store = store.read().await;
Ok(store.inited())
})
}
}
impl NotificationStore for PostgresNotificationStore {
fn list_notification_guids_for_job_and_state(
&mut self,
job: JobId,
state: JobState,
) -> Pin<Box<dyn Future<Output = Result<Vec<NotificationId>, JobSchedulerError>> + Send>> {
let store = self.store.clone();
let table = self.table.clone();
let states_table = self.states_table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::CantListGuids),
PostgresStore::Inited(store) => {
let store = store.read().await;
let state = state as i32;
let sql = "SELECT DISTINCT states.id \
FROM \
"
.to_string()
+ &*table
+ " as states \
RIGHT JOIN "
+ &*states_table
+ " as st ON st.id = states.id \
WHERE \
job_id = $1 \
AND state = $2";
let result = store.query(&*sql, &[&job, &state]).await;
match result {
Ok(rows) => Ok(rows
.iter()
.map(|r| {
let uuid: Uuid = r.get(0);
uuid
})
.collect::<Vec<_>>()),
Err(e) => {
error!("Error listing notification guids for job and state {:?}", e);
Err(JobSchedulerError::CantListGuids)
}
}
}
}
})
}
fn list_notification_guids_for_job_id(
&mut self,
job_id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<Vec<Uuid>, JobSchedulerError>> + Send>> {
let store = self.store.clone();
let table = self.table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::CantListGuids),
PostgresStore::Inited(store) => {
let store = store.read().await;
let sql =
"SELECT DISTINCT id FROM ".to_string() + &*table + " WHERE job_id = $1";
let result = store.query(&*sql, &[&job_id]).await;
match result {
Ok(rows) => Ok(rows
.iter()
.map(|g| {
let uuid: Uuid = g.get(0);
uuid
})
.collect::<Vec<_>>()),
Err(e) => {
error!(
"Error getting list of notifications guids for job id{:?}",
e
);
Err(JobSchedulerError::CantListGuids)
}
}
}
}
})
}
fn delete_notification_for_state(
&mut self,
notification_id: Uuid,
state: JobState,
) -> Pin<Box<dyn Future<Output = Result<bool, JobSchedulerError>> + Send>> {
let store = self.store.clone();
let states_table = self.states_table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::CantRemove),
PostgresStore::Inited(store) => {
let store = store.read().await;
let state = state as i32;
let sql = "DELETE FROM ".to_string()
+ &*states_table
+ " \
WHERE \
id = $1 \
AND state = $2 \
RETURNING state";
let result = store.query(&*sql, &[&notification_id, &state]).await;
match result {
Ok(row) => Ok(!row.is_empty()),
Err(e) => {
error!("Error deleting notification for state {:?}", e);
Err(JobSchedulerError::CantRemove)
}
}
}
}
})
}
fn delete_for_job(
&mut self,
job_id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let store = self.store.clone();
let table = self.table.clone();
Box::pin(async move {
let store = store.read().await;
match &*store {
PostgresStore::Created(_) => Err(JobSchedulerError::CantRemove),
PostgresStore::Inited(store) => {
let store = store.read().await;
let sql = "DELETE FROM ".to_string() + &*table + " WHERE job_id = $1";
store
.query(&*sql, &[&job_id])
.await
.map(|_| ())
.map_err(|e| {
error!("Error deleting for job {:?}", e);
JobSchedulerError::CantRemove
})
}
}
})
}
}
@@ -0,0 +1,254 @@
use crate::JobSchedulerError;
use crate::context::Context;
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::{JobState, JobType};
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::{JobState, JobType};
use chrono::{FixedOffset, Utc};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::sync::oneshot::{Receiver, Sender};
use tracing::error;
use uuid::Uuid;
pub struct Scheduler {
pub shutdown: Arc<AtomicBool>,
pub start_tx: Arc<RwLock<Option<Sender<bool>>>>,
pub start_rx: Arc<RwLock<Option<Receiver<bool>>>>,
pub ticking: Arc<AtomicBool>,
pub inited: bool,
}
impl Default for Scheduler {
fn default() -> Self {
let (ticker_tx, ticker_rx) = tokio::sync::oneshot::channel();
Self {
shutdown: Arc::new(AtomicBool::new(false)),
inited: false,
start_tx: Arc::new(RwLock::new(Some(ticker_tx))),
start_rx: Arc::new(RwLock::new(Some(ticker_rx))),
ticking: Arc::new(AtomicBool::new(false)),
}
}
}
impl Scheduler {
pub async fn init(&mut self, context: &Context) {
if self.inited {
return;
}
let job_activation_tx = context.job_activation_tx.clone();
let notify_tx = context.notify_tx.clone();
let job_delete_tx = context.job_delete_tx.clone();
let shutdown = self.shutdown.clone();
let metadata_storage = context.metadata_storage.clone();
self.inited = true;
let start_rx = {
let mut w = self.start_rx.write().await;
w.take()
};
let ticking = self.ticking.clone();
tokio::spawn(async move {
let is_ticking = ticking.load(Ordering::Relaxed);
if !is_ticking {
if let Some(start_rx) = start_rx {
if let Err(e) = start_rx.await {
error!(?e, "Could not subscribe to ticker starter");
return;
}
}
let is_ticking = ticking.load(Ordering::Relaxed);
if !is_ticking {
loop {
let is_ticking = ticking.load(Ordering::Relaxed);
if is_ticking {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
}
'next_tick: loop {
let shutdown = {
let r = shutdown.load(Ordering::Relaxed);
r
};
if shutdown {
break 'next_tick;
}
tokio::time::sleep(Duration::from_millis(500)).await;
let now = Utc::now();
let next_ticks = {
let mut w = metadata_storage.write().await;
w.list_next_ticks().await
};
if let Err(e) = next_ticks {
error!("Error with listing next ticks {:?}", e);
continue 'next_tick;
}
let mut next_ticks = next_ticks.unwrap();
let to_be_deleted = next_ticks.iter().filter_map(|v| {
v.id.as_ref()?;
if v.next_tick == 0 {
let id: Uuid = v.id.as_ref().unwrap().into();
Some(id)
} else {
None
}
});
for uuid in to_be_deleted {
let tx = job_delete_tx.clone();
tokio::spawn(async move {
if let Err(e) = tx.send(uuid) {
error!("Error sending deletion {:?}", e);
}
});
}
next_ticks.retain(|n| n.next_tick != 0);
let must_runs = next_ticks.iter().filter_map(|n| {
let next_tick = n.next_tick_utc();
let last_tick = n.last_tick_utc();
let job_type: JobType = JobType::from_i32(n.job_type).unwrap();
let must_run = match (last_tick.as_ref(), next_tick.as_ref(), job_type) {
(None, Some(next_tick), JobType::OneShot) => {
let now_to_next = now.cmp(next_tick);
matches!(now_to_next, std::cmp::Ordering::Greater)
|| matches!(now_to_next, std::cmp::Ordering::Equal)
}
(None, Some(next_tick), JobType::Repeated) => {
let now_to_next = now.cmp(next_tick);
matches!(now_to_next, std::cmp::Ordering::Greater)
|| matches!(now_to_next, std::cmp::Ordering::Equal)
}
(None, Some(next_tick), JobType::Cron) => {
let now_to_next = now.cmp(next_tick);
matches!(now_to_next, std::cmp::Ordering::Greater)
|| matches!(now_to_next, std::cmp::Ordering::Equal)
}
(Some(last_tick), Some(next_tick), _) => {
let now_to_next = now.cmp(next_tick);
let last_to_next = last_tick.cmp(next_tick);
(matches!(now_to_next, std::cmp::Ordering::Greater)
|| matches!(now_to_next, std::cmp::Ordering::Equal))
&& (matches!(last_to_next, std::cmp::Ordering::Less)
|| matches!(last_to_next, std::cmp::Ordering::Equal))
}
_ => false,
};
if must_run {
let id: Uuid = n.id.as_ref().map(|f| f.into()).unwrap();
Some(id)
} else {
None
}
});
for uuid in must_runs {
{
let tx = notify_tx.clone();
tokio::spawn(async move {
if let Err(e) = tx.send((uuid, JobState::Scheduled)) {
error!("Error sending notification activation {:?}", e);
}
});
}
{
let tx = job_activation_tx.clone();
tokio::spawn(async move {
if let Err(e) = tx.send(uuid) {
error!("Error sending job activation tx {:?}", e);
}
});
}
let storage = metadata_storage.clone();
tokio::spawn(async move {
let mut w = storage.write().await;
let job = w.get(uuid).await;
let next_and_last_tick = match job {
Ok(Some(job)) => {
let job_type: JobType = JobType::from_i32(job.job_type).unwrap();
let schedule = job.schedule();
let fixed_offset = FixedOffset::east_opt(job.time_offset_seconds)
.unwrap_or(FixedOffset::east_opt(0).unwrap());
let now = now.with_timezone(&fixed_offset);
let repeated_every = job.repeated_every();
let next_tick = job
.next_tick_utc()
.map(|nt| nt.with_timezone(&fixed_offset));
let next_tick = match job_type {
JobType::Cron => {
schedule.and_then(|s| s.iter_after(now).next())
}
JobType::OneShot => None,
JobType::Repeated => repeated_every.and_then(|r| {
next_tick.and_then(|nt| {
nt.checked_add_signed(chrono::Duration::seconds(
r as i64,
))
})
}),
};
let last_tick = Some(now);
Some((
next_tick.map(|nt| nt.with_timezone(&Utc)),
last_tick.map(|nt| nt.with_timezone(&Utc)),
))
}
_ => {
error!("Could not get job metadata");
None
}
};
if let Some((next_tick, last_tick)) = next_and_last_tick {
if let Err(e) =
w.set_next_and_last_tick(uuid, next_tick, last_tick).await
{
error!("Could not set next and last tick {:?}", e);
}
}
});
}
}
});
}
pub async fn shutdown(&mut self) {
self.shutdown.swap(true, Ordering::Relaxed);
}
pub async fn start(&mut self) -> Result<(), JobSchedulerError> {
let is_ticking = self.ticking.load(Ordering::Relaxed);
if is_ticking {
Err(JobSchedulerError::TickError)
} else {
self.ticking.swap(true, Ordering::Relaxed);
let tx = {
let mut w = self.start_tx.write().await;
let mut tx: Option<Sender<bool>> = None;
std::mem::swap(&mut tx, &mut *w);
tx
};
if let Some(tx) = tx {
if let Err(e) = tx.send(true) {
error!(?e, "Start ticker send error");
}
}
Ok(())
}
}
}
@@ -0,0 +1,150 @@
use crate::JobSchedulerError;
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::{JobAndNextTick, JobStoredData};
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::{JobAndNextTick, JobStoredData};
use crate::store::{DataStore, InitStore, MetaDataStorage};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
pub struct SimpleMetadataStore {
pub data: Arc<RwLock<HashMap<Uuid, JobStoredData>>>,
pub inited: bool,
}
impl Default for SimpleMetadataStore {
fn default() -> Self {
Self {
data: Arc::new(RwLock::new(HashMap::new())),
inited: false,
}
}
}
impl DataStore<JobStoredData> for SimpleMetadataStore {
fn get(
&mut self,
id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<Option<JobStoredData>, JobSchedulerError>> + Send>>
{
let data = self.data.clone();
Box::pin(async move {
let r = data.write().await;
let val = r.get(&id).cloned();
Ok(val)
})
}
fn add_or_update(
&mut self,
data: JobStoredData,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let id: Uuid = data.id.as_ref().unwrap().into();
let job_data = self.data.clone();
Box::pin(async move {
let mut w = job_data.write().await;
w.insert(id, data);
Ok(())
})
}
fn delete(
&mut self,
guid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let job_data = self.data.clone();
Box::pin(async move {
let mut w = job_data.write().await;
w.remove(&guid);
Ok(())
})
}
}
impl InitStore for SimpleMetadataStore {
fn init(&mut self) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
self.inited = true;
Box::pin(std::future::ready(Ok(())))
}
fn inited(&mut self) -> Pin<Box<dyn Future<Output = Result<bool, JobSchedulerError>> + Send>> {
let val = self.inited;
Box::pin(std::future::ready(Ok(val)))
}
}
impl MetaDataStorage for SimpleMetadataStore {
fn list_next_ticks(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<Vec<JobAndNextTick>, JobSchedulerError>> + Send>> {
let data = self.data.clone();
Box::pin(async move {
let r = data.read().await;
let ret = r
.iter()
.map(|(_, v)| (v.id.clone(), v.next_tick, v.last_tick, v.job_type))
.map(|(id, next_tick, last_tick, job_type)| JobAndNextTick {
id,
next_tick,
last_tick,
job_type,
})
.collect::<Vec<_>>();
Ok(ret)
})
}
fn set_next_and_last_tick(
&mut self,
guid: Uuid,
next_tick: Option<DateTime<Utc>>,
last_tick: Option<DateTime<Utc>>,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let data = self.data.clone();
Box::pin(async move {
let mut w = data.write().await;
let val = w.get_mut(&guid);
match val {
Some(val) => {
val.set_next_tick(next_tick);
val.set_last_tick(last_tick);
Ok(())
}
None => Err(JobSchedulerError::UpdateJobData),
}
})
}
fn time_till_next_job(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<Option<std::time::Duration>, JobSchedulerError>> + Send>>
{
let data = self.data.clone();
Box::pin(async move {
let r = data.read().await;
let now = Utc::now();
let now = now.timestamp() as u64;
let val = r
.iter()
.filter_map(|(_, jd)| match jd.next_tick {
0 => None,
i => {
if i > now {
Some(i)
} else {
None
}
}
})
.min()
.map(|t| t - now)
.map(std::time::Duration::from_secs);
Ok(val)
})
}
}
@@ -0,0 +1,8 @@
mod metadata_store;
mod notification_store;
mod to_code;
pub use metadata_store::SimpleMetadataStore;
pub use notification_store::SimpleNotificationStore;
pub use to_code::SimpleJobCode;
pub use to_code::SimpleNotificationCode;
@@ -0,0 +1,248 @@
use crate::JobSchedulerError;
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::{JobIdAndNotification, JobState, NotificationData};
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::{JobIdAndNotification, JobState, NotificationData};
use crate::job::{JobId, NotificationId};
use crate::store::{DataStore, InitStore, NotificationStore};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
pub struct SimpleNotificationStore {
pub data: Arc<RwLock<HashMap<Uuid, HashMap<Uuid, NotificationData>>>>,
pub notification_vs_job: Arc<RwLock<HashMap<Uuid, Uuid>>>,
pub inited: bool,
}
impl Default for SimpleNotificationStore {
fn default() -> Self {
Self {
data: Arc::new(RwLock::new(HashMap::new())),
notification_vs_job: Arc::new(RwLock::new(HashMap::new())),
inited: false,
}
}
}
impl InitStore for SimpleNotificationStore {
fn init(&mut self) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
self.inited = true;
Box::pin(std::future::ready(Ok(())))
}
fn inited(&mut self) -> Pin<Box<dyn Future<Output = Result<bool, JobSchedulerError>> + Send>> {
let val = self.inited;
Box::pin(std::future::ready(Ok(val)))
}
}
impl DataStore<NotificationData> for SimpleNotificationStore {
fn get(
&mut self,
id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<Option<NotificationData>, JobSchedulerError>> + Send>>
{
let data = self.data.clone();
let job = self.notification_vs_job.clone();
Box::pin(async move {
let job = job.read().await;
let job = job.get(&id);
match job {
Some(job) => {
let val = data.read().await;
let val = val.get(job);
match val {
Some(job) => {
let val = job.get(&id).cloned();
Ok(val)
}
None => Err(JobSchedulerError::GetJobData),
}
}
None => Err(JobSchedulerError::GetJobData),
}
})
}
fn add_or_update(
&mut self,
data: NotificationData,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let jobs = self.notification_vs_job.clone();
let notifications = self.data.clone();
Box::pin(async move {
let id = data.job_id.as_ref();
match id {
Some(val) => {
let JobIdAndNotification {
job_id,
notification_id,
} = val;
match (job_id, notification_id) {
(Some(job_id), Some(notification_id)) => {
let job_id: Uuid = job_id.into();
let notification_id: Uuid = notification_id.into();
let mut jobs = jobs.write().await;
jobs.insert(notification_id, job_id);
let mut notifications = notifications.write().await;
notifications.entry(job_id).or_insert_with(HashMap::new);
let job = notifications.get_mut(&job_id);
if let Some(job) = job {
job.insert(notification_id, data);
}
Ok(())
}
_ => Err(JobSchedulerError::UpdateJobData),
}
}
None => Err(JobSchedulerError::UpdateJobData),
}
})
}
fn delete(
&mut self,
guid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let jobs = self.notification_vs_job.clone();
let notifications = self.data.clone();
Box::pin(async move {
let job_id = {
let r = jobs.read().await;
r.get(&guid).cloned()
};
let mut jobs = jobs.write().await;
match job_id {
Some(job_id) => {
jobs.remove(&guid);
let mut notifications = notifications.write().await;
let job = notifications.get_mut(&job_id);
match job {
Some(job) => {
job.remove(&guid);
if job.is_empty() {
notifications.remove(&job_id);
}
Ok(())
}
None => Err(JobSchedulerError::CantRemove),
}
}
None => Err(JobSchedulerError::CantRemove),
}
})
}
}
impl NotificationStore for SimpleNotificationStore {
fn list_notification_guids_for_job_and_state(
&mut self,
job_id: JobId,
state: JobState,
) -> Pin<Box<dyn Future<Output = Result<Vec<NotificationId>, JobSchedulerError>> + Send>> {
let state: i32 = state.into();
let notifications = self.data.clone();
Box::pin(async move {
let notifications = notifications.read().await;
let job = notifications.get(&job_id);
match job {
Some(job) => Ok(job
.iter()
.filter_map(|(k, v)| {
if v.job_states.contains(&state) {
Some(*k)
} else {
None
}
})
.collect::<Vec<_>>()),
None => Ok(vec![]),
}
})
}
fn list_notification_guids_for_job_id(
&mut self,
job_id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<Vec<Uuid>, JobSchedulerError>> + Send>> {
let notifications = self.data.clone();
Box::pin(async move {
let notifications = notifications.read().await;
let job = notifications.get(&job_id);
match job {
Some(job) => Ok(job.iter().map(|(k, _v)| *k).collect::<Vec<_>>()),
None => Ok(vec![]),
}
})
}
fn delete_notification_for_state(
&mut self,
notification_id: Uuid,
state: JobState,
) -> Pin<Box<dyn Future<Output = Result<bool, JobSchedulerError>> + Send>> {
let state: i32 = state.into();
let jobs = self.notification_vs_job.clone();
let notifications = self.data.clone();
Box::pin(async move {
let mut ret = false;
let job_id = {
let r = jobs.read().await;
r.get(&notification_id).cloned()
};
let mut jobs = jobs.write().await;
match job_id {
Some(job_id) => {
let mut notifications = notifications.write().await;
let job = notifications.get_mut(&job_id);
match job {
Some(job) => {
if job.contains_key(&notification_id) {
let notification = job.get_mut(&notification_id).unwrap();
if notification.job_states.contains(&state) {
ret = true;
}
notification.job_states.retain(|v| *v != state);
if notification.job_states.is_empty() {
job.remove(&notification_id);
jobs.remove(&notification_id);
}
}
if job.is_empty() {
notifications.remove(&job_id);
}
Ok(ret)
}
None => Err(JobSchedulerError::CantRemove),
}
}
None => Err(JobSchedulerError::CantRemove),
}
})
}
fn delete_for_job(
&mut self,
job_id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let jobs = self.notification_vs_job.clone();
let notifications = self.data.clone();
Box::pin(async move {
let mut jobs = jobs.write().await;
jobs.retain(|_k, v| *v != job_id);
let mut notifications = notifications.write().await;
notifications.remove(&job_id);
Ok(())
})
}
}
@@ -0,0 +1,227 @@
use crate::context::{Context, NotificationDeletedResult};
use crate::job::JobToRunAsync;
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::{JobIdAndNotification, JobState, NotificationData};
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::{JobIdAndNotification, JobState, NotificationData};
use crate::job::to_code::{JobCode, NotificationCode, ToCode};
use crate::{JobSchedulerError, JobStoredData, OnJobNotification};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::broadcast::{Receiver, Sender};
use tracing::{error, warn};
use uuid::Uuid;
pub type LockedJobToRunMap = Arc<RwLock<HashMap<Uuid, Arc<RwLock<Box<JobToRunAsync>>>>>>;
pub type LockedNotificationToRunMap =
Arc<RwLock<HashMap<Uuid, Arc<RwLock<Box<OnJobNotification>>>>>>;
pub struct SimpleJobCode {
pub job_code: LockedJobToRunMap,
}
impl Default for SimpleJobCode {
fn default() -> Self {
SimpleJobCode {
job_code: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl SimpleJobCode {
async fn listen_for_additions(
data: LockedJobToRunMap,
mut rx: Receiver<(JobStoredData, Arc<RwLock<Box<JobToRunAsync>>>)>,
) {
loop {
let val = rx.recv().await;
if let Err(e) = val {
error!("Error receiving {:?}", e);
break;
}
let (JobStoredData { id: job_id, .. }, val) = val.unwrap();
let uuid: Uuid = job_id.as_ref().unwrap().into();
let mut w = data.write().await;
w.insert(uuid, val);
}
}
async fn listen_for_removals(
data: LockedJobToRunMap,
mut rx: Receiver<Result<Uuid, (JobSchedulerError, Option<Uuid>)>>,
) {
loop {
let val = rx.recv().await;
if let Err(e) = val {
error!("Error receiving job removal {:?}", e);
break;
}
let uuid = val.unwrap();
if let Ok(uuid) = uuid {
let mut w = data.write().await;
w.remove(&uuid);
}
}
}
}
impl ToCode<Box<JobToRunAsync>> for SimpleJobCode {
fn init(
&mut self,
context: &Context,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let data = self.job_code.clone();
let job_create = context.job_create_tx.subscribe();
let job_deleted = context.job_deleted_tx.subscribe();
Box::pin(async move {
tokio::spawn(SimpleJobCode::listen_for_additions(
data.clone(),
job_create,
));
tokio::spawn(SimpleJobCode::listen_for_removals(data, job_deleted));
Ok(())
})
}
fn get(
&mut self,
uuid: Uuid,
) -> Pin<
Box<
dyn Future<Output = Result<Option<Arc<RwLock<Box<JobToRunAsync>>>>, JobSchedulerError>>
+ Send,
>,
> {
let data = self.job_code.clone();
Box::pin(async move {
let r = data.read().await;
Ok(r.get(&uuid).cloned())
})
}
}
impl JobCode for SimpleJobCode {}
pub struct SimpleNotificationCode {
pub data: LockedNotificationToRunMap,
}
impl Default for SimpleNotificationCode {
fn default() -> Self {
Self {
data: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl SimpleNotificationCode {
async fn listen_for_additions(
data: LockedNotificationToRunMap,
mut rx: Receiver<(NotificationData, Arc<RwLock<Box<OnJobNotification>>>)>,
tx: Sender<Result<Uuid, (JobSchedulerError, Option<Uuid>)>>,
) {
loop {
let val = rx.recv().await;
if let Err(e) = val {
error!("Error receiving {:?}", e);
break;
}
let (uuid, val) = val.unwrap();
let uuid: Uuid = {
match uuid {
NotificationData {
job_id:
Some(JobIdAndNotification {
notification_id: Some(job_id),
..
}),
..
} => job_id.into(),
_ => continue,
}
};
{
let mut w = data.write().await;
w.insert(uuid, val);
}
if let Err(e) = tx.send(Ok(uuid)) {
warn!("Error sending notification created {:?} {:?}", e, uuid);
}
}
}
// TODO check for elsewhere
async fn listen_for_removals(
data: LockedNotificationToRunMap,
mut rx: Receiver<(Uuid, Option<Vec<JobState>>)>,
tx: Sender<NotificationDeletedResult>,
) {
loop {
let val = rx.recv().await;
if let Err(e) = val {
error!("Error receiving job removal {:?}", e);
break;
}
let (uuid, states) = val.unwrap();
error!(
"Removing notification uuid {:?} and not caring about states!",
uuid
);
{
let mut w = data.write().await;
w.remove(&uuid);
}
if let Err(e) = tx.send(Ok((uuid, true, states))) {
error!("Error sending notification removed {:?} {:?}", e, uuid)
}
}
}
}
impl ToCode<Box<OnJobNotification>> for SimpleNotificationCode {
fn init(
&mut self,
context: &Context,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>> {
let data = self.data.clone();
let rx_create = context.notify_create_tx.subscribe();
let tx_created = context.notify_created_tx.clone();
let rx_delete = context.notify_delete_tx.subscribe();
let tx_deleted = context.notify_deleted_tx.clone();
Box::pin(async move {
tokio::spawn(SimpleNotificationCode::listen_for_additions(
data.clone(),
rx_create,
tx_created,
));
tokio::spawn(SimpleNotificationCode::listen_for_removals(
data, rx_delete, tx_deleted,
));
Ok(())
})
}
fn get(
&mut self,
uuid: Uuid,
) -> Pin<
Box<
dyn Future<
Output = Result<Option<Arc<RwLock<Box<OnJobNotification>>>>, JobSchedulerError>,
> + Send,
>,
> {
let data = self.data.clone();
Box::pin(async move {
let r = data.read().await;
Ok(r.get(&uuid).cloned())
})
}
}
impl NotificationCode for SimpleNotificationCode {}
@@ -0,0 +1,28 @@
use crate::JobSchedulerError;
use crate::job::JobToRunAsync;
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::{JobAndNextTick, JobStoredData};
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::{JobAndNextTick, JobStoredData};
use crate::store::{CodeGet, DataStore, InitStore};
use chrono::{DateTime, Utc};
use std::future::Future;
use std::pin::Pin;
use uuid::Uuid;
pub trait MetaDataStorage: DataStore<JobStoredData> + InitStore {
fn list_next_ticks(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<Vec<JobAndNextTick>, JobSchedulerError>> + Send>>;
fn set_next_and_last_tick(
&mut self,
guid: Uuid,
next_tick: Option<DateTime<Utc>>,
last_tick: Option<DateTime<Utc>>,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>>;
fn time_till_next_job(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<Option<std::time::Duration>, JobSchedulerError>> + Send>>;
}
pub trait JobCodeGet: CodeGet<Box<JobToRunAsync>> {}
@@ -0,0 +1,52 @@
use crate::JobSchedulerError;
use std::future::Future;
use std::pin::Pin;
use uuid::Uuid;
mod metadata_store;
mod notification_store;
pub use metadata_store::MetaDataStorage;
pub use notification_store::NotificationStore;
pub trait InitStore {
fn init(&mut self) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>>;
fn inited(&mut self) -> Pin<Box<dyn Future<Output = Result<bool, JobSchedulerError>> + Send>>;
}
pub trait DataStore<DATA>
where
DATA: Sized,
{
fn get(
&mut self,
id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<Option<DATA>, JobSchedulerError>> + Send>>;
fn add_or_update(
&mut self,
data: DATA,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>>;
fn delete(
&mut self,
guid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>>;
}
pub trait CodeGet<CODE>
where
CODE: Sized,
{
fn get(
&mut self,
id: Uuid,
) -> Box<dyn Future<Output = Result<Pin<Box<CODE>>, JobSchedulerError>>>;
fn notify_on_add(
&mut self,
id: Uuid,
) -> Box<dyn Future<Output = Result<(), JobSchedulerError>>>;
fn notify_on_delete(
&mut self,
id: Uuid,
) -> Box<dyn Future<Output = Result<(), JobSchedulerError>>>;
}
@@ -0,0 +1,36 @@
#[cfg(not(feature = "has_bytes"))]
use crate::job::job_data::{JobState, NotificationData};
#[cfg(feature = "has_bytes")]
use crate::job::job_data_prost::{JobState, NotificationData};
use crate::job::{JobId, NotificationId};
use crate::store::{CodeGet, DataStore, InitStore};
use crate::{JobSchedulerError, OnJobNotification};
use std::future::Future;
use std::pin::Pin;
use uuid::Uuid;
pub trait NotificationStore: DataStore<NotificationData> + InitStore {
fn list_notification_guids_for_job_and_state(
&mut self,
job: JobId,
state: JobState,
) -> Pin<Box<dyn Future<Output = Result<Vec<NotificationId>, JobSchedulerError>> + Send>>;
fn list_notification_guids_for_job_id(
&mut self,
job_id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<Vec<Uuid>, JobSchedulerError>> + Send>>;
fn delete_notification_for_state(
&mut self,
notification_id: Uuid,
state: JobState,
) -> Pin<Box<dyn Future<Output = Result<bool, JobSchedulerError>> + Send>>;
fn delete_for_job(
&mut self,
job_id: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), JobSchedulerError>> + Send>>;
}
pub trait NotificationRunnableCodeGet: CodeGet<Box<OnJobNotification>> {}