#ChronoGrapher - One Unified Scheduler, Unlimited Power
1 messages · Page 5 of 1
also i've split the parsing to a TimeLiteral for parsing individual literals
this way. we can reuse the logic in future macros
Why?
honestly i don't provide a getter method so part of me has some fault there
but
basically
since we have Duration which is consistent, its better to trust it vs just doing the schedule method and getting the duration from there
its this close...
im removing Vec<T> allocations from timing wheel and even in the SchedulerEngine
in an effort to reduce temporary objects and improve performance
apparently its gonna be problematic with SegQueue
With what do you bencmark?
a rookie script
in main_cg.rs
`struct MyTaskFrame;
#[async_trait]
impl TaskFrame for MyTaskFrame {
type Error = Box<dyn TaskError>;
async fn execute(&self, _ctx: &TaskFrameContext) -> Result<(), Self::Error> {
yield_now().await;
COUNTER.fetch_add(1, Ordering::Relaxed);
Ok(())
}
}
pub async fn benchmark_chronographer() {
println!("LOADING TASKS");
let t = tokio::time::Instant::now();
let scheduler = Scheduler::<DefaultSchedulerConfig<Box<dyn TaskError>>>::default();
const EXEC_TIMES: usize = 6;
const TASKS_ALLOCATED: usize = 450_000;
let spread_millis = 1000.0 / ((TASKS_ALLOCATED * EXEC_TIMES) as f64);
let mut millis = 0f64;
for _ in 0..TASKS_ALLOCATED {
millis = (millis + spread_millis).rem_euclid(1000.0);
let task = Task::new(
TaskScheduleInterval::duration(Duration::from_millis(millis.round() as u64)),
MyTaskFrame
);
let _ = scheduler.schedule(task).await;
}
scheduler.start().await;
println!("STARTED {}", t.elapsed().as_secs_f64());
}
then in main.rs
mod main_cg;
mod main_tokio;
pub static COUNTER: LazyLock<AtomicUsize> = LazyLock::new(|| AtomicUsize::new(0));
pub async fn benchmark() {
let mut last = COUNTER.load(Ordering::Relaxed);
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open("tasks_per_sec.csv")
.unwrap();
writeln!(file, "time_sec,tasks_per_sec").unwrap();
for i in 0..=50 {
tokio::time::sleep(Duration::from_secs(1)).await;
let delta = COUNTER.swap(0, Ordering::SeqCst);
println!("{}", i);
writeln!(file, "{:.2},{:.2}", i, delta).unwrap();
}
}
#[tokio::main(flavor = "multi_thread", worker_threads = 16)]
#[allow(clippy::empty_loop)]
async fn main() {
benchmark_chronographer().await;
benchmark().await;
}
guys...
Honestly imma take a beak
i really can't work for some reason, i have pumped some stuff
but im just not as productive as i was
that doesn't mean ofc work won't be done
@fervent lark can you tell me about ConditionalFrame i am writing doc for with_condition
it executes the inner TaskFrame based on a predicate function
if it returns true
if it returns false, then depending on whenever you specified to return an errror or just success it will do that
optionally you can specify a secondary TaskFrame to execute upon a falsey value
should be mentuioned the predicate function is not a boolean
okay
ngl with the ui tests, i really don't like em
they are a bit cumbersome to mantain
i don't have any other solution in mind, so this is the best we have
i have a very hacky idea for CRON expressions for the cron! macro
extract the tokens, convert them to string and pass it down to TaskScheduleCron::from_str to create the instance
mapping any errors from Result into compile-time ones
then take the fields of the CRON, and pasting them into the macro i presume
that way we have one source of truth
@placid drum your with_fallback method is perfect
like 10/10 perfect
This part feels eh
/// ... the result depends on how the underlying [`ConditionalFrame`] is configured; it
/// can either return an error, or just resolve as a success without executing the task. In the context of `with_condition`,
/// it acts as a no-operation and returns a success by default upon a falsey value.
It bores the user with many details
on with_dependency you don't list the other method
fixed and merged the PR
savior
@fervent lark you change almost everything
you can comment on pr so i can improve furthur
same
feels bit hacky
but it doesnt seems that there is other way to do it
ye...
tbh there were some severe issues, look once the core comes out il be more strict with things
if i were to comment, well i'd have to wait for you to change it then wait for me to notice and review then merge
wwhich takes time
ye thats the issue sadly
why dont you delete the mem leak issue ?
Iteration 495198: Memory: 986 KB (Delta: 0 KB)
Final memory: 986 KB
Total leaked: 0 KB
Memory growth was minimal.
imma kind of bored
im also considering another approach for handling Tasks
which (probably) will naturally delete
Delete what?
The leak is gone
Or i didnt see an API update
true, i will try my best
the mem leak
it will naturally be gone after the TaskHandle approach im gonna take
But it is already deleted
ok
wait wdym its already deleted?
it still exists
Task doesn't notify the hook registry to remove the hooks
Maybe my bench is not up to date
this is complicated... Im trying to set up an epoch-based storage solution
the idea is as follows
the SchedulerTaskStore is a fixed array of shards. The number of shards that live in a store are (cpus * 4).next_power_of_two()
each shard has its own buffer inside (a Vec<T> wrapped in an RwLock) and a free list. (The entire buffer is wrapped in an Arc<T> for reasons explained below)
Each element is a slot in the Vec<T> managed by epoch-based GC
When a new Task is pushed, the store checks which shards have a free slot by linearly searching (walks through) all shards, optionally we could use a global SegQueue by im not sure about that. If it doesn't find any then it picks a random shard to allocate increase its buffer size via RwLock to fit in an extra slot for the Task (which is kind of expensive but quite rare so it can be avoided)
if it does find though a free slot, then it replaces the Slot's contents with the Task. Each Slot has a generational counter which just counts how many times the slot has been reallocated to prevent ABA-related problems (even though epochs can manage it for us, combing both approach can lead to better throughput theoritically). The generational counter is incremented every time a slot is reused (from free it becomes owned)
then when a Task is allocated on the specific BufferShard, the generation and index is returned. Then we create a EphemeralTaskRef<C> which stores the index and generation PLUS a Weak<BufferShard> (that was why we needed the Arc<T>)
and its returned for the user to clone and use as much as they want
for reading slots, we simply use this shard reference, peek in the buffer with the index to get its contents, check for the generational counters matching (if not then its invalid) and simply return the contents
for removing a Task it marks the slot as free and pushes it on the free list
i've made use of hazard pointers over epoch based GC
one of the issues is TaskFrameContext, i need some effecient way to be able to send commands to the TaskHandle and let it execute but not expose the TaskHandle inside the TaskFrameContext as it would require C: SchedulerConfig everywhere
one of the ideas is to use a command queue-based system but this can get expensive if i manually create one myself and delete it, alternatively each Task could have its own queue pre-made but still expensive
the idea is to use the workers to do this for me perhaps
instead of yknow, rambling...
man the TaskFrameContext is one of the bigger problems
and ofc il be doing this stuff alone...
ye it did
which is why i put a [HEAVY WIP]
@placid drum btw
what full form of WIP
yes
ye, we'd need help
we are doing this
and we need well help bc of how hard the problems are with this migration
well not we, mostly me, @jolly frost doesn't respond since he has a busy schedule
and the only other active contributor is you really
honestly the docs can wait, this one is quite important change imo
i'll try to look into code
look in the diagrams and if you need anything let me know, should i explain the problem and the 2 solutions and then the difficulties of the TaskHandle solution or things are suffecient?
i'll check and let you know what i need
ok
@fervent lark can you give me issue ?
wdym by issue?
forgot it
anyway i was going to delete comment but reply
if you mean gh issues, well
thanks
look on the alternatives
but its not in full detail
il write up in more detail the approach
that would be wonderful
ok so the problems as you understand is basically How do we reference and manipulate a Task instance in outside code. Think like TaskDependency or anything else similar which may require a task instance and use it
It should be mentioned in the system, the Scheduler should OWN the Task instance when scheduled and later on (just for other outside code we need a reference mechanism)
now the 2 approaches are Task<T1, T2> -> TaskIdentifier. It involves us creating a Task<T1, T2> instance with the TaskFrame / TaskTrigger which has its own TaskHooks, this is typed and can be shared around via &Task<T1, T2>
however it has to be delivered to a Scheduler to take an effect, and you have to pass it by ownership. Which then returns a TaskIdentifier
you have to carry around the Scheduler and the identifier, and basically you can call Scheduler operations and supply the identifier as an argument
in the diagram, towards left are the pros/cons of this approach
at the top of the diagram is how its layed out
should be mentioned the typed task cannot be cloned around but identifiers can be cloned
then there are TaskHandles, they are sort of the TaskIdentifier. Basically you immediately ask a Scheduler given a TaskFrame and a TaskTrigger to create a Task instance, then it returns a TaskHandle<C> which allows you to interact with said instance without ever knowing the Scheduler directly.
Not only it allows task-based operations such as getting TaskFrame / TaskTrigger and working with TaskHooks. It also allows Scheduler based operations such as scheduling, cancelling... etc. This handle can be cloned around freely
the TaskHandle<C> isn't a trait but rather a struct which contains a TaskRef, this is where the SchedulerTaskStore comes in play. Imagine this, the store declares how the task instance is meant to be interacted with and then the handle wraps this along with the other Scheduler composites and provides the Scheduler based operations on top
its a 2 step process
again look the diagrams for pros/cons and how its layed out
@placid drum is this suffecient to explain the system?
ya
this should be sufficient
also one of the issues tackled in both solutions are What happens if a Task is removed and a reference to it is still kept around, don't worry about this one since its already solved but just know it exists
should also mention, focus on the TaskFrameContext problem
@placid drum you've come up perhaps with any solution?
man for the love of god, if i try to ask for help from the outside community apart from us. They'd need to either dig through the code which most won't, or i'd have to explain everything in a manifesto format which no one reads and ignores or omit details on purpose which then peps ask about the problem. What the fuck is this TaskHandle problem 
ITS BEEN 2 WEEKS OF BANGING MY HEAD CONTINIOUSLY
i've opened yet another help post https://discord.com/channels/273534239310479360/1486110679378428095
i really, and i mean really wanna move on from this problem
im litterally going insane over one problem
like i try this, fails, i try that it fails. Send help
Did you try on rust forum?
I'll ask
@fervent lark here we can get help https://users.rust-lang.org/t/architecture-for-referencing-and-interacting-tasks-around/139150
i am asking here because we were stuck at this for long time. Summary Currently im faced with a problem i've been battling for 2 weeks straight. In my system i have "Tasks" which contains their own TaskFrame, TaskTrigger and TaskHooks (a collection of them). Then i have "Schedulers" which are meant to own Tasks, the problem im faced goes as fol...
nope tbh
i posted you can check
ye i checked it out
at least this guy suggested stuff tbh
unlike leaving us blind
il do need then to explain the problem differently
solution what is think - can we keep state and then we can access that, but we have to pass that down the line like state in axum, what do you think ?
give a sec
ya sure take your time
hmmm il need to edit it tbh
i gotta admit, i like this transprency and honesty instead of ghosting
as now i can just improve yknow
do let them know il repost this on my own with a clearer description
ya give me link your post i'll let him know
its gonna be complicated to figure out how to best describe the issue
il try to tackle the problem at a different angle
so i've figured out a sort of system
the idea is there will be 2 systems for communicating with the Scheduler
the command system and the query system
everything happens through the SchedulerWorker
each worker has its own command/query system
can we update task through worker ?
should i delete ticket or still need help ?
eh no, don't
wdym?
il try today to implement the above system
this is fucking hell
i've created a new branch
to do all sorts of stuff with the handles
if we can't really resolve this, then i will honestly delete the branch
best of luck
why there is lot of option to do same thing in rust ahh just learning makes headach
eh there ain't lots and lots
correct but some i am just getting confuse
the trippy stuff is this problem 
its so fucking hard to abstract the config out and not require a generic at all
just end my misery and let the TaskHandle approach work as i want it to be 
litterally i beg to stop this misery
man... I have no other idea than to either embrace this which has problems with TaskHooks or something else 
tbh since we don't make any progress
lets forget the TaskHandle based approach and return to Task<T1, T2> with a couple of adjustments
No worries, it wasn’t rude or needy. We have a target to achieve after all. I will submit my thesis on June, so it might not be possible before that because I am falling behind schedule on the thesis right now. I haven’t answered because discord was banned in my country for the last two weeks 😄
oh jess
tbh we definitely won't get there at the deadline
ok i fucking give up
like the problem is hard to explain properly
i litterally don't know how to formulate it without writing a manifesto
im out of ideas and its honestly a huge roadblock
honestly i really fear the project's core will close in 1 year, 1 YEAR
a horrible sign
we have like 3.5 years to deliver this project to a presentable shape (which has cloud support and distributed systems stuff, plus other stuff im lazy to mention)
honestly il set a deadline for the TaskHandle API, if well we don't get it through April 1st, we'll just ditch it entirely
is this project still open for contribution? im not sure i have the prerequisite knowledge but based on the original message i am potentially interested, and i have a decent amount of free time at the moment
it is
though i do reccomend like a strong foundation in Rust, design patterns and so on in order to be most useful. But the minimal stuff is like 5-6 months of experience in Rust to be somewhat viable for the team
currently the most useful thing you can provide is being a design partner, essentially helping with how the API feels and its architecture, then comes unit tests, documentation and implementing some systems
from the jist of it, you seem to be somewhat viable for the team
Also should note that currently the biggest roadblock is designing the TaskHandle API, which is why rn design partners are heavily needed
okay, interesting. how should i go about catching myself up?
well mostly about experience, you just learn patterns and stuff more and more to get familiar with how to design a good API (plus ofc practicing). But given your current experience, you might be able to help out with some basic stuff perhaps like the docs / unit tests
i meant, catching myself up in terms of this project specifically
ah
well
there are some issues you can try to tackle
and read the codebase i guess, but im not sure how well you can understand it, depends on your experience level. Just know that because we are working on this one, the code won't compile for now (temporary). Everything you may need me to explain for or clear things up, ask me freely
@supple maple il work on some diagrams to better explain the architecture, if you want, just let me know
that might be useful, for now ill just read through the codebase and examples
sure
again lmk if you need any help, im open to questions / guidance
i guess my primary issue is that its a huge codebase and i only have a vague idea of what a scheduler is (i havent used one extensively in the past). some broad explanations would be useful, if you dont mind
like, my understanding from the examples and what ive previously encountered is that you define a bunch of tasks (in the form of functions) and then use the proc macros (if youre using the rust api) to define when and how tasks should run. then you use the scheduler api to make a scheduler and then pass tasks into it
ye sort of like that
haven't warned you yet, but ig you might have seen it from the README, the macros aren't yet ready. So far they are a conceptual idea
Il work on it tommorow, in like 18 hours or so from now on
ok so @supple maple, il get to work on explaining the concepts
should i go broad or any specifics you need?
broad
here are the two sides
both have 3 components that make them up
they can coordinate between each other (with the exception of TaskTrigger and TaskFrame, plus TaskTrigger and TaskHooks but this might change). It should be noted SchedulerEngine has its own component called SchedulerClock but it isn't managed directly by the Scheduler, rather the SchedulerEngine
Let's start from the Task side of things, in basic terms a Task is a unit in which the Scheduler manages to execute at a specific time (via its TaskTrigger). Now Tasks are more powerful than just "A function with a schedule" via various patterns
TaskFrames are conceptually the code in which you execute, though they are more powerful as they can define workflows. Workflows in simple terms are TaskFrames wrapping one and the other (decorating pattern from OOP). Execution works from top to bottom, each TaskFrame controls itself how to execute nested ones
ChronoGrapher already provides its own various TaskFrames (more specifically they are called workflow primitives). Examples may include:
RetriableTaskFrameRetries the nested part up to "N" times with a specified delay (or immediate)TimeoutTaskFrameEnforces a time limit on the nested partFallbackTaskFrameExecutes the first nested part, if it fails then it executes the second nested part as a fallback. If both fail it fails entirely.
<...>
on their own they are simple, the power comes when chaining these simple primitives with your code
interesting
NOTE 1# The order matters significantly, chaining
retry->fallbackis NOT the same asfallback->retry. While it may seem an annoynace / nuasance of ChronoGrapher at first, its what allows for basiccally infinite types of workflows. You may want to execute a fallback to catch common errors without retrying or a global fallback for any errors that leaked through
NOTE 2# You can stack multiple
retry,fallbackor any other workflow primitive even on top of each other, this is especially useful as some workflow primitives have their own scope. For example you can stack a globaltimeoutfor the entire workflow and nest atimeouton a fallback to only enforce this time limit on that fallback
by "retries" do you mean it repeats it or only retries in the case of an error?
yup
which one?
the second
okay interesting
how would you make a task repeat several times (regardless of if it succeeds or not)?
then there are TaskTriggers, these allow you to define WHEN you want to execute the Task, they are also just functions but they are supposed to remain more simple and barebones compared to TaskFrames. ChronoGrapher also provides its own primitives such as:
TaskScheduleImmediatefor immediate executionTaskScheduleIntervalfor basic interval-based executionTaskScheduleCronfor CRON-based expressions (ex.* * * * ? *, follows Quartz style cron)TaskScheduleCalendarfor more complex scheduling rules
NOTE: You may notice i use
TaskScheduleand notTaskTrigger. There is aTaskScheduletrait which implements theTaskTriggertrait automatically, the main difference isTaskTriggermay sit idle for a while and only announce the time it calculates when it feels like it. WhereasTaskScheduleimmediately announces it
- The former is more non-deterministic and non-immediate (like an API request or monitering)
- The latter is more mathematical / computational (like an interval or CRON)
wdym by that?
like repeatedly schedule and then execute?
How do you make those diagrams
if you mean this, a Task by default can execute infinitely. The cycle goes:
- Task is scheduling (figuring out its own time)
- Task announces the time -> Scheduler sorts it and fills it in a slot of time
- Scheduler waits around, executing other Tasks in the meantime
- Task's time is due, so Scheduler starts executing
- Repeat
i gtg, il be back in 15-30 minutes. Do let me know @supple maple what questions you have, any confusions and so on so i can resolve those
alr, sounds good
okay so like if i can say a concrete example
lets say the task MyTask just involves printing Hi to the console, and I want to print Hi to the console 10 times at once every hour
let me check my intuition here
let say i have tasks T, U. then retry (fallback T, U) 2 (making up my own notation) would try T, if it failed try U, and if both of those failed, try T again, and if that failed then try U again, and finally if that failed the whole thing fails.
fallback (retry T 2) U would try T, if it failed try T again, if that failed try U, and if that failed the whole thing fails
so the first one tries T -> U -> T -> U and the bottom T -> T -> U
you would need TaskScheduleCalendar for that
or you could make your own TaskSchedule that does just that
for a fallback, T and U are TaskFrames NOT Tasks, but apart from that your logic is good
okay what if i wanted a task T to run, output some integer N, and then run task U N times
you mean standalone Tasks or just TaskFrames?
if you mean TaskFrames ig just tell frames since its kind of ambigious what you want
what im getting at is can taskframes like communicate with/influence each other
yes, sort of
there is an API called Shared Objects, this allows a parent TaskFrame to declare a shared object which is then passed down to its child TaskFrames, this object can be accessed by these child objects in read-mode (with interrior mutability, you can make it mutable as well)
the shared object can be in any shape of data, it can be a single integer, a struct, an enum... etc.
they work like React's context API
NOTE 1# There can be multiple shared objects of different types, for example a TaskFrame
Tcould have a shared object calledMyTwhereasUcould have a shared objectMyU. Each shared object has its own scope of where it can be accessed. For instance if i define at the leaf of the workflow a shared object, the parentTaskFramecan't really access it (thinking about it, they can with some hacky workarounds, but its really an anti-pattern).
NOTE 2# If there is a case where there are multiple
T, with their ownMyTshared objects nesting each other (directly or indirectly). Then only the latest / closest / last shared object is accessible, on other scopes it could be the first, second.. etc.
NOTE 3# An internal detail but worth mentioning, the shared object API is not itself a true API in the sense but a wrapper around TaskHooks. Which means you can replicate the API yourself if you want via TaskHooks, its just a nicer wrapper
these are the backbone of ChronoGrapher when it comes to extensibility (you will see these around on various integrations, extensions and so on. Even in the core they are used internally), il start with the problem:
"Suppose I have a Task with a complex workflow, how can I listen to / observe whats happening when its running?"
They mostly solve this issue where they listen to a specified number of events defined (and attached, there are 2 phases to this), HOWEVER they are far more powerful and allow for patterns such as state management (additional data can be embedded on a Task), markers (same as state management, but with no data, just they mark a Task via their presence), post-error handling (by listening to when a Task ends, you can try to resolve an error with indirect means)
consider something like this (taken from the README)
struct PrometheusMetricsHook;
This is a TaskHook used for integrating with Prometheus, its a dummy example and there will be a serious extension on top later down the line.
I want this TaskHook to react to specific events, for our case its 4. We can do:
impl TaskHook<OnTaskStart> for PrometheusMetricsHook {
async fn on_event(&self, event: OnTaskStart, ctx: Arc<TaskContext>, payload: &OnTaskStart::Payload) {
// ...Increment the number of running Tasks and update metrics...
}
}
For listening to when a Task is about to start
impl TaskHook<OnTaskEnd> for PrometheusMetricsHook {
async fn on_event(&self, event: OnTaskEnd, ctx: Arc<TaskContext>, payload: &OnTaskEnd::Payload) {
// ...Decrement the number of running Tasks and update metrics...
}
}
For listening to when a Task is about to end
impl TaskHook<OnTimeout> for PrometheusMetricsHook {
async fn on_event(&self, event: OnTimeout, ctx: Arc<TaskContext>, payload: &OnTimeout::Payload) {
// ...Executes when a TimeoutTaskFrame throws a timeout...
}
}
For listening when a TimeoutTaskFrame reports a timeout error
impl TaskHook<OnHookAttach<OnTaskStart>> for PrometheusMetricsHook {
async fn on_event(
&self,
event: OnHookAttach<OnTaskStart>,
ctx: Arc<TaskContext>,
payload: &OnHookAttach<OnTaskStart>::Payload
) {
// ...You can initialize logic for when it is attached to a OnTaskStart event...
}
}
For listening to when that hook is attached to a OnTaskStart event.
Then you have the second phase where you attach those events to a Task like so:
let hook = Arc::new(PrometheusMetricsHook);
task.attach_hook::<OnTaskStart>(hook).await;
task.attach_hook::<OnTimeout>(hook).await;
every event may contain a payload, this payload can be used to extract info about the event and what happened.
there are various patterns to TaskHooks. First of, if you want you can generalize a method to listen to specific events like so:
impl<E: TaskHookEvent> TaskHook<E> for PrometheusMetricsHook {
async fn on_event(&self, ...) {...}
}
This method will activate for every type of TaskHookEvent, it doesn't know directly the type of event it got activated from. Though it allows for you to attach it onto any type of event.
Though be warned you do have to manually attach the events you want, this is sadly one thing i do have in my mind to fix
However if you want more restrictions to impose on what kinds of events are allowed (helpful for narrowing, there are THEGs (TaskHookEvent Groups). They are just traits that implement TaskHookEvent and may contain a specific payload type shape (but depends).
An example of this are the Task lifecycle event:
// Only the OnTaskStart and OnTaskEnd is allowed, NOTHING ELSE
impl<E: TaskLifecycleEvents> TaskHook<E> for PrometheusMetricsHook {
async fn on_event(&self, ...) {...}
}
Another pattern is Hook-To-Hook communication. TaskHooks can basically communicate with one and another via their own sets of events (yes you can create your own events just like how the core does). The idea is a TaskHook emits an event and some other TaskHook implements the event method to listen to (and ofc gets attached)
TaskHooks can also inspect if there are TaskHooks around, attach their own or detach some specific ones. But its not limited to TaskHooks, ANYTHING can achieve those activities without any restriction (just know the type of TaskHook you want to act upon), even the Scheduler, even TaskFrames... etc.
There is also a conceptual idea, not yet formed. But you may ask yourself:
"What happens if I have 2 or more same type TaskFrames emitting events and I want to narrow which TaskFrame is allowed to emit those events?"
Well introducing (not yet) the Mute & Transform pattern, there is a special TaskFrame called MuteTaskFrame, it allows you to define various events to mute (effectively if any nested part emits those events, it never gets through to the TaskHooks, its a middleman).
Now the interesting thing you can do is take those muted events and capture them (you can run side effects per event). You can emit one or more events back if you want (which is where the transform comes in).
The pattern basically involves being able to run specific events on a certain scope in a filter which will then emit different event(s), or i guess mute it entirely if you want.
To get an example of how this pattern works, suppose the workflow:
TaskFrameA
|- TaskFrameB
|- TaskFrameC
|- TaskFrameB
|- TaskFrameD
And then suppose TaskFrameB fires a MyEventB even for TaskHooks to listen, but i don't want the nested B (with the sibling of D) fire, so i can change the workflow to:
TaskFrameA
|- TaskFrameB
|- MuteTaskFrame
|- TaskFrameC
|- TaskFrameB
|- TaskFrameD
now only the top most TaskFrameB gets to emit freely MyEventB. Depending on how you set up MuteTaskFrame, you could re-emit the event as say MyEventB2 or something along the lines, or you can fully mute it. You can of course put different mutes in your workflow all tuned with their own settings
@supple maple you got the idea i presume?
ok ok good
dw
i mean with that said you basically know the entire side of Tasks, like all the various patterns and stuff, everything
btw you have used any job scheduler / distributed task queue / workflow orchestrator before? (ex. Celery, Temporal, Apache Airflow, BullMQ... etc.)
btw the number of general concepts are like:
- TaskFrames
- TaskTriggers / TaskSchedules
- TaskHooks
- Shared Data API (but really its just TaskHooks so it might not count)
Each with their own sets of patterns (except TaskTrigger and Shared Data API)
i dont think so
what are TaskHandles? you mentioned that earlier
its a shift in how things are managed, in the previous model you created a Task object right? This contained your TaskHooks, TaskFrame, TaskTrigger and so on right? Then you deliver this Task onto the Scheduler for it to store it somewhere for scheduling and stuff (fully owned)
then the Scheduler returns a TaskIdentifier which using it and the Scheduler, you can trace that Task back and do all sorts of modifications
such as cancelling a Task, removing it fully, rescheduling it or immediately executing
also i forgot to mention SchedulerHandle (yes, its also a TaskHook). In the previous model this was used as a proxy between Tasks and the Scheduler to allow all sorts of operations from anywhere within the workflow, this TaskHook is private but the TaskFrameContext object does expose methods to interact with it and send instructions
now the idea of TaskHandle is what if we skipped this step and unify things?
this model has severeal pros / cons, which you can check it out here, btw the problem more precisely is:
"How do I reference and interact with a Task in outside code (may include TaskFrames, TaskHooks... etc.)?"
these 2 models are just approaches (the more i think about it, the more it may be better to mix them, keep the intermediate pattern but empower the identifier in some way). Either way this is still an open ended discussion and being implemened (or at least trying to) as we speak
ngl imma pin this to guide other people like you who have questions since ARCHITECTURE.md is somewhat outdated and not enough information is currently availaible such as API docs and the Guidebook (due to WIP status)
yea thats a good diagram
okay, how does this work? like you and the scheduler have shared ownership of the task?
sort of
the TaskHandle is a middleman, it gives you methods in which you can operate upon the Task. WITHOUT knowing any details about the scheduler side
this middleman can be cloned around and freely shared. Currently the idea is
^
is it just like an Arc<..>
kinda
except i don't pay much of the cost of Arc<T> (unless i want to, depends)
mainly the costs are cloning it around and memory wise
here is the memory model ^
in summary its a slotmap (custom implementation for high concurrency). And basically the only stuff i store are 2 usizes + a u16 (though may change to a u32)
so 10 bytes per key
whereas the Arc<T> is 8 bytes per instance
wait...
actually the u16 will remain as that nvm
oh shit, apparently SlotKey is more expensive in memory around 24 bytes
but the good thing is if you don't use it, the memory is freed so ye...
previously the idea was to use a DashMap<usize, Arc<ErasedTask>>
initially with zero entries, the slotmap uses 16 bytes over dashmap's 40
the slotmap in terms of allocation, is also quite faster than dashmap
actually nvm
same-ish speed
for blocking_allocate
dashmap uses btw ~59.68MB, the slotmap uses ~24.77MB which is nuts
and there might be a chance i can compress it even further
also lookup is faster vs a dashmap, unless you use Arc<T> which matches the speed but uses more of the memory
oh shit...
apparently without Arc<T>, my slotmap is heavier
ye il fix this
great recap
thx
tbh for now idc much, il implement the system then optimize
actually nah imma optimise the slotmap
tbh fuck the TaskHandle based approach
we'll revert the system
done
@supple maple
you can run ChronoGrapher like normal, forget about the TaskHandle appoach and shenanigans (though its being considered to add this back in the future)
ngl these 2 issues are some of the hardest to get right:
https://github.com/GitBrincie212/ChronoGrapher/issues/154
https://github.com/GitBrincie212/ChronoGrapher/issues/141
What area has an architectural problem? Please describe. The Task area specifically allocating Tasks to the Scheduler and referencing them elsewhere. Why does this area have this architectural prob...
imma work on https://github.com/GitBrincie212/ChronoGrapher/issues/150, the idea is users toggle via compiler features tokio, smol or even use sync. I provide primitives for spawning, declaring functions and so on via macros and depending which feature is set, that section is active which is compatible with what we want
basically ChronoGrapher's code is being "rewritten" to support other runtimes than just tokio
I'm removing the TaskSchedule trait btw
its just an alias really and its more confusing than good
the code won't be the prettiest thing
you will see like lots of macro magic used
also 90% of ChronoGrapher is being rewritten to support multiple runtimes
actually no
there are a few reasons for not doing it apparently
btw @supple maple howz progression going? What you planning to do?
sorry, some school stuff came up, ill be much more free starting this weekend
definitely still interested tho
ok
pushed some changes https://github.com/GitBrincie212/ChronoGrapher
mainly rewriting the ARCHITECTURE.md and removing TaskSchedule in favour of TaskTrigger
i've finished the ARCHITECTURE.md document
btw @placid drum and @jolly frost ig its best to help out on the Task stuff. Honestly since we can't really unify the APIs, its best to split them in two just like before. However, now instead of an identifier and the need to carry the Scheduler around, im gonna mix in the TaskHandle based approach
the idea is explained in ARCHITECTURE.md
bascially Task<T1, T2> now is a temporary representation, its ineffecient since its just basic storage and not meant to interacted very frequently. Then once it gets stored on the Scheduler, it destructures it revealling all information this container held, reassembles it to its own representation (for optimization) and then stores it. Afterward returning the handle
can you assign issue describing issue and solution if you know this would give me right amount of information to get started
its supposed to be https://github.com/GitBrincie212/ChronoGrapher/issues/154
but
its not updated with the new approach
what you need btw in terms of info?
oh and also i've created a branch to do the handle stuff https://github.com/GitBrincie212/ChronoGrapher/tree/handles
why i know chronographer codebase but there is something missing , what i am missing i don't know
give me anything i'll somehow manage
just forgot i'll look into it
i'll ask if needed
ok
the ARCHITECTURE.md in the Lifecycle of Tasks section should explain conceptually anything you need
thanks
btw
one of the issues we will again encounter is how would the TaskFrameContext communicate with the Scheduler WITHOUT knowing anything about the Scheduler
i know basic
is this enough ?
by basics wdym?
well the basic insense, what its is use and other mentioned in doc and ARCHITECTURE.md
ok
man quite lazy
im also thinking about the "Mute & Transform" pattern
btw im thinking if there is a better data structure for TaskHooks that results in constant fetch time with the operations i have
for the slotmaps, one idea is 2 DashMaps plus a slotmap, the slotmap contains the TaskHooks whilst the first DashMaps map TaskHook type to the slot position, the second maps event to TaskHooks
i do imagine an edge case though on the top of my head
let hook1: Arc<MyHook> = Arc::new(...);
let hook2: Arc<MyHook> = Arc::new(...);
// ...
task.attach_hook::<MyEvent1>(hook1.clone()); // MyEvent1 -> [hook1]
task.attach_hook::<MyEvent2>(hook1.clone()); // MyEvent2 -> [hook1]
task.attach_hook::<MyEvent2>(hook2.clone()); // MyEvent1 -> [hook1, hook2]
Since MyHook produces the same TypeId regardless of which instance, if i emit MyEvent1, how do i know which instance corresponds? Its supposed to be hook1 but since i've appended hook2 it would run that thinking its the last instance added onto the event when its not
solving this is tricky, SlotMap only allows reading contents from slots, not being able to modify them
Guys we gotta get movin more
we're supposed to have 16 days to finish the project's core (NOT including the SDKs and other features)
but i will need to delay the deadline
my brain is refuses to braining
my brain also does that but we keep goin
ladies and gentlemen
LADIES AND GENTLEMEN
we've done it
over 2 million Tasks per second on average
2.1x faster than tokio_schedule
2.1 million now
and its rising
for context btw
where blue is ChronoGrapher and orange is tokio_schedule
the main compromise is now the scheduler composites are no longer object safe, though i do think some weren't and besides its not their intended use case as even if you make them trait objects, you'd need the SchedulerConfig, so... A worthwhile tradeoff
What was previous avg?
1.7 million
there is lots of potential for optimization via async fn, i've used impl Future<Output = ()> + Send which has increased performance but it may be better to switch back to async fn with adjustments to make sure cache friendliness, a smaller state machine footprint... etc.
its kinda crazy how i've improved the performancce to be over 2.1x faster than tokio_schedule
and over 10x the performance from the initial benchmark
i've also got an idea for TaskHooks to fully avoid Arc<T>
what if we apply the same handle-based approacch over there as well?
let hook = ctx.instantiate_hook(MyHook::new(...))
hook.subscribe::<MyEvent1>()
hook.subscribe::<MyEvent2>()
something like this
hey this issue open ? https://github.com/GitBrincie212/ChronoGrapher/issues/154
yes its still open
and honestly we'd need to do this
just give max 1hour i finish work in hand work on this
its just i wanna get started with chronographer but i am not getting enough time
oh
btw im creating a new issue for TaskHooks
how is it difficulty compare to with https://github.com/GitBrincie212/ChronoGrapher/issues/154 ?
Does the issues are all up to date?
TaskHooks may be easier
yup, most likely
but the Task based approach is more important imo
What are the ones where I could help?
currently minimal
i mean docs and unit tests
ok
oh yeah and you can help with ig https://github.com/GitBrincie212/ChronoGrapher/issues/146
though this does require a bit more experience than just unit tests and docs
but not much
oh btw btw
ofc as mentioned, do read https://github.com/GitBrincie212/ChronoGrapher/blob/master/ARCHITECTURE.md
ya sure
also for the slotmap im gonna make an issue
I can do that?
not really
Sry, but how do you run your benchmarks ? Like bench and tests on bin and benches and make test gives nothing. Or it is that bin/ contains the benches ?
make test probably isn't updated
typically you do cargo test
but now you gotta cd into the folder to run the command
In benches?
Why does not it gives me any file or info during comp about the benches ?
hold on
Above contains warnings
Why am i that lost 😂
Wait
Does not it haves something to do with target?
i've updated the makefile
Let me see
What should make bench output?
Why there is no [[bench]] attr in benchs toml file?
It may be why ? Divan docs indicates to indicates benches there
@fervent lark can i try this https://github.com/GitBrincie212/ChronoGrapher/issues/165
ye
ig
though this does involve the above resolving issue ^
so which to work on first
probably might be better to work on this
we have to make it mostly lock-free for most performance (memory and runtime)
okay i'll work on this one
it may be better to remove the hazard pointers as well but not sure
ok
btw important
do write a small benchmark script
and run it vs DashMap
okay
hold on il give one benchmark script myself
take your time i'll sleep then work on this one
ok so i've written it
interestingly
=== DashMap Initialization ===
MEMORY USED: 0.008192MB
RUNTIME: 72.585µs
=======
=== SlotMap Initialization ===
MEMORY USED: 0.024576MB
RUNTIME: 37.234µs
=======
=== DashMap Allocations ===
MEMORY USED: 17.826816MB
RUNTIME: 207.784582ms
=======
=== SlotMap Allocations ===
MEMORY USED: 12.39276MB
RUNTIME: 221.595047ms
=======
=== DashMap Clear ===
MEMORY USED: 0.0MB
RUNTIME: 5.436083ms
=======
=== SlotMap Clear ===
MEMORY USED: 0.0MB
RUNTIME: 45.899µs
=======
=== DashMap Deallocations ===
MEMORY USED: 0.0MB
RUNTIME: 86.878349ms
=======
=== SlotMap Deallocations ===
MEMORY USED: -7.833848MB
RUNTIME: 274.703377ms
=======
it uses slightly less memory but the runtime is a lil more expensive
No transcript returned for file voice-message.ogg in 17.708546ms (0.520 second file)
i've updated the benchmarks
here is the script
we might need to review how slotmap implements its SlotMap, learn from there and adapt it to be concurrent ready
Why would there be a change ?
Like what are the difference between those 2 ?
the slotmap crate's SlotMap isn't fully optimized for concurrency
yes you can use a single Mutex<T> to make it accessible through but this is bad for performance due to contention, a better approach is sharding the slotmap for multiple locks
but even then, i think it can be made better
also im gonna make a new issue about TaskTriggers
im thinking for a TaskTrigger to act as a state machine, switching between other TaskTriggers
the idea is for it to be a macro that generates the state machine TaskTrigger implementation
so should work on it or not ?
probably not
tbh try to document something
the SlotMap has to be mostly lock-free which is where tons of issues begin
oh okay thanks
it involves lots of complexities
like using an intrusive linked list, more formally a trieber stack as a linked list
apparently the only the problems with this is how MyHook can't listen to HookAttach<MyHook> and can't initialize its logic once it got attached
but not a huge problem though as most of the time you initialize the logic earlier on like a constructor
im gonna have issues with TaskHooks...
Overview The documentation for ChronoGrapher provides a clear and concise example for using the library in Rust. However, there is a lack of similar examples for Python integration, which could lea...
seems like an AI.
EDIT: Of course it is, its not like i can get anything good quality on this world because of fuckass AI
another issue i find is via Arc<T>, TaskHook instances can be shared amongst Tasks
maybe its best to keep instances task-local
and for TaskHooks that need a shared state across Tasks, they could use a registry-based pattern
ye so there will be changes in TaskHooks, from now on, basically everything implements TaskHook<()>
which means you can do cursed stuff like this:
let hook = ctx.instantiate_hook(3usize);
but meh its an anti-pattern in most cases
imma commit my changes
on the handles branch
@placid drum @jolly frost you can view what approach im going with
slotmap ?
not that
the handles based approach
okay
btw i should mention one of the changes not mentioned in ARCHITECTURE.md is how TaskHooks now behave
first of everything is a so called "stale" TaskHook, these are meant to contain only metadata / state
stale TaskHooks are either TaskHook<()> or via the alias StaleTaskHook
second when attaching a TaskHook, you need "ownership" of the TaskHook instance (this can be circumvented il explain later)
and once attachement is done, a handle is returned. Now this handle is used to interact with the TaskHook instance
you can subscribe an instance to multiple events at a time, or even unsubscribe them
this is fire 
you can also detach them from the handle or get their instance as a reference
more importantly one change is you can emit an event for ONE TaskHook
so task side let say i wanna change taskframe can i do that ?
well is it delivered to the Scheduler?
or not?
if not you can change it as much as you would like, though at some point you will have to deliver it to a Scheduler
these are the local handle methods, there are also global methods which act upon the ENTIRE container
you can emit an event for all TaskHooks to listen
you can fully detach a TaskHook from all of its events by knowing its type (or via a single event)
interesting
you can get an instance of a TaskHook either from its type or if you want more specificity from its type and event
NOTE
these methods are different
suppose we have one TaskHook type and two instances, lets call those A and B, we attach A to an event called MyEvent1 and MyEvent2, then we attach B MyEvent1
when you fetch via event, lets say MyEvent2 you get A
BUT
when you fetch globally you get B
both the get hook methods fetch the last instance of a TaskHook's type, what counts as a last instance is their difference
the global methods are defined in the handle of a Task
whereas the local ones are defined by the TaskHookHandle
make sense also why we get B not A, i am not able to understand..
B is the last to be attached
hence we get B
whereas here, we track which instance of the hook type is last attached TO THE EVENT
if you have say
let task1: TaskHandle<...>;
let hook1: TaskHookHandle<...>;
task1.emit_event::<T>(); // Acts upon the entire container
hook1.emit::<T>(); // Acts upon only the TaskHook and nothing else
when we gonna write test cases ?
later
also for the methods i specifically refer to get_hook_from<E, T> (used for getting the last instance of type T that subscribed to the event E) and get_hook<T> (used for getting the last instance of type T)
these are global
hence only accessible through TaskHandle
hmm understood
also
i will start writing doc if you have any intermediate level issue open do let me know
architectural issue is out off league for now.
ok
i do advise for docs
ideally don't touch Scheduler stuff
nor Task stuff (not the TaskFrames, just directly)
okay
actually imma tell you what to document
base/src/task/trigger/schedule/calendar.rs
base/src/task/dependency.rs
base/src/task/dependency
these are some
okay
i like progressive clock, well i really wanted when i am using cron for testing things.
oh ye you can do that
ya i am appreciating it, i read it in code the other day.
wait
my brain ain't braining much
you meant virtual clock, since you are testing things deterministically right?
TaskHooks are a huge problem...
we'd have to get a bit crafty on how TaskHooks behave 
this is gonna be a tricky problem
we need the following:
- Getting the last TaskHook instance GLOBALLY based on
TaskHooktype - Getting the last TaskHook instance LOCALLY from an event based on
TaskHooktype and the event's type - Attaching a TaskHook instance GLOBALLY
- Detaching the last TaskHook instance GLOBALLY
- Subscribing a TaskHook instance (from the attachement) LOCALLY from an event type
- Unsubscribing a TaskHook instance (from the attachement) LOCALLY from an event type
- Iterate over all TaskHooks based on an event
Yes
ye its quite a lil neat tool
man the registry-based stuff will be extremely hard to make it as optimal as possible
honestly if i get the new design to like over 4-5 million Tasks per second, il quit the optimizations since its enough
and i want it to be extremely effecient
good luckk
ye il definitely need it
Why is it so so SO hard to design the SchedulerTaskStore with effeciency in mind 
WHAT THE FUCK
NO WAY
@placid drum
i reverted to the standard approach, after one change i did
holy shit
wait
i spawned 450k Tasks that execute about 6 times per second
which is chronographer.
nice improvement from this.
the math litterally doesn't add up
how do you benchmark ?
450k times 6 should be at best 2.7 million
isnt this good ?
no?
it surpassed 2.7 million
which means duplicate Tasks ran
struct MyTaskFrame;
#[async_trait]
impl TaskFrame for MyTaskFrame {
type Error = Box<dyn TaskError>;
async fn execute(&self, _ctx: &TaskFrameContext) -> Result<(), Self::Error> {
yield_now().await;
COUNTER.fetch_add(1, Ordering::Relaxed);
Ok(())
}
}
pub async fn benchmark_chronographer() {
println!("LOADING TASKS");
let t = tokio::time::Instant::now();
let scheduler = Scheduler::<DefaultSchedulerConfig<Box<dyn TaskError>>>::default();
const EXEC_TIMES: usize = 6;
const TASKS_ALLOCATED: usize = 450_000;
let spread_millis = 1000.0 / ((TASKS_ALLOCATED * EXEC_TIMES) as f64);
let mut millis = 0f64;
for _ in 0..TASKS_ALLOCATED {
millis = (millis + spread_millis).rem_euclid(1000.0);
let task = Task::new(
TaskScheduleInterval::duration(Duration::from_millis(millis.round() as u64)),
MyTaskFrame
);
let _ = scheduler.schedule(task).await;
}
scheduler.start().await;
println!("STARTED {}", t.elapsed().as_secs_f64());
}
also here is the script
very weirdly removing the yield
freezes the program entirely
Why is tokio_scheduler that slow?
i've been spawning multiple tokio tasks for each Task
ig that could contribute to how slow it is
very weid
actually let me zoom it more
i've rewritten the benchmark to basically increase the Tasks by a batch (1000) every second, each scheduling every 10 milliseconds
caught a bug
caugh a bunch actually
btw i realize the timing wheel can be made lock-free
actually its lock-free but
it does require a central queue though
which can be a bottleneck
the naive approach is to define SegQueue instead of Vec<T>
but
its costly in memory, around 327KB for one timing wheel 256b * 256 * 5
i will need to make the data structure as minimal in memory as possible
They've deleted the issue lol
seems like im running to multiple issues with concurrent data structures, instead of bloating ChronoGrapher in the utils folder with the data structures, it may be best to create a new crate / project (the aformentioned conckit)
@fervent lark
error[E0603]: constant `TIME_FIELD` is private
--> proc/src/every.rs:5:47
|
5 | use crate::utils::time_literal::{TimeLiteral, TIME_FIELD};
| ^^^^^^^^^^ private constant
|
note: the constant `TIME_FIELD` is defined here
--> proc/src/utils/time_literal.rs:82:1
|
82 | const TIME_FIELD: [&str; 5] = ["milliseconds", "seconds", "minutes", "hours", "days"];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
getting this error for master branch
okay t
merged
btw im thinking about the slot map (this will be availaible as another crate and NOT just in ChronoGrapher though it will be used), here is how i would lay it out:
The slot map is AtomicPtr<[Segment<T>]>, basically a Vec<T> without the length/capacity information (thats stored alongside it). Now each segment is conceptually the same as the above just a Vec<T> stripped of its length/capacity information but the difference is now we are in the slots. Each slot contains a SlotContents and a packed AtomicU32 containing a generational counter in the 30 lower bits + a state in the 2 higher bits. Where:
00= Free01= Reserved11= Occupied
SlotContents is a union, that either contains the value or points to the next free slot, this is concluded via the state above. Now an interesting problem arises
"How do we track free slots?"
This is where we can use an Intrusive Linked List. BUT, the change is we don't store it per segment, no no, thats ineffecient. We store a global linked list where the index usize encodes both the segment and the index of the slot relative to the segment. Now another problem arises:
"What happens when 2 slots are free but live in different segments?"
The solution is to encode in the SlotContents where the segment lives. One idea i had is if the free index is out of range then it means it lives in another segment so we just div & modulo by the segment size to find the offset of segments and the slot it lives in. Then from there navigate
The linked list is of course a frieber stack otherwise it would be hella expensive to navigate to the end of the linked list. Another problem arises though with the free slots
"How do we free memory when there are free slots but on different segments?"
The answer is we don't, but what we do is bias the allocation towards segments that are more full, leaving empty-ish ones to slowly drain and once fully drained then remove
honestly its an internal detail and a low level one at that
i will learn by seeing your implementation.
lets just say its gonna be complex 
good luck, that's i can do for now
also this will be in the backlog
turns out going lock-free may not be optimal
there are very specific cases where going lock-free makes sense for performance
so this arch is final ?
?
we were changing architecture right ?
fixed some bugs
and rewrote the benchmark
now the benchmark measures throughput over time as Tasks are added (1k per second)
the results are ||not supring lol||
blue for tokio_schedule, purple for ChronoGrapher
ChronoGrapher at worst is slightly faster than tokio_schedule
on average its 3x faster
and on best case even 4x
now granted, as i pointed out to @formal sedge, tokio_schedule is slower since i always spawn a tokio task per Task which can easily get out of hand petty quick
also the throughput dropped to 400k, as the previous benchmark wasn't correct, it benchmarked thoughput as Tasks executed instantly
@formal sedge suprisingly, even if i allocate a batch of 64 tokio Tasks that act as workers and then the schedules get pushed there, it still doesn't improve it
tbh i think its a hard cap of 1 million Tasks per second for some reason
Performance could be improved even more by sharding the Hierarchical Timing Wheel
the idea is make the workers poll the engine, instead of just a single SegQueue pushing the commands and having one thread manage ticking (single-threaded)
@jolly frost We should look toggether on how best to shard the timing wheel
the goal is for ticking to be consistent (exactly ~1ms) but allow workers to fetch Tasks in parallel and process
and improve the work stealing code
Honestly this is one of those changes that will increase massively performance
very suprisingly:
i've added 2 more benchmarks
tokio_cron_scheduler in green
and a pure tokio based interval loop is purple
and ChronoGrapher outperforms them all in most scenarios
i have omitted some other crates, i tried to benchmark on crates like tsuki_scheduler but their model just doesn't map cleanly
their API kinda sucks ngl (no bias but it actually doesn't feel clean), neither is my base API but the macros will be significantly cleaner for sure
the frustrating bit is how their docs are unmantained and how i can't use it in a multi-threaded scenario
Is your feature request related to a problem? Please describe. Currently, there are basic TaskTrigger (scheduling) primitives such as intervals, cron expressions... etc. But usually users may want ...
@placid drum tbh, it may be better to touch on docs, if we roll out the handles based approach then we will definitely need to rewrite it but for now should be good
okay i'll start
@placid drum you like the VirtualClock right? Well you will love this even more:
https://github.com/GitBrincie212/ChronoGrapher/issues/170
and they can be combined for extreme deterministic replays
it allows you to record exactly every step the Scheduler took
I feel like it can be made even more powerful
giving the ability to basically inspect through composites, not just in the Scheduler though this will be hard to get right
oh shit, super sorry blake for the wrong ping just realized it now xd
I have zero memory of anything going on here lol
XDDD
im actually sorry
didn't meant to mention you, wanted to mention @placid drum but ye
i'll look into it thanks btw
ngl man, we do need to finish the core asap
we supposedly had like 8 days to finish the core
and basically all of us are slacking back (for me personally its just irl stuff and a bit of the burnout effect)
i do wish we had more manpower (specifically a bit more of expertises, not asking like a PhD but neither asking for basically blind workers, i do wish we just had more of it) for the project
my plan -
- rename Scheduler -> LiveScheduler (update all refrence)
- simulacrum will become part of LiveScheduler
and then processed on @fervent lark should i work on this
eh probably not for now
this is more of a backlog
i suggest better do docs
Ok
also forgot, one issue is the website, we haven't even touched it which we should. Honestly the main force blocking me from writing the guidebook docs is https://github.com/GitBrincie212/ChronoGrapher/issues/141
its a hard problem that has to go
If we find a way then things can go faster
can we use TaskFrameContext ?
it will require lot of changes
means ?
well we can't just shove say a generic or a field just for the FallbackTaskFrame
what if i make a FallbackTaskFrame2 (not realistic but suppose something else that does require the system the fallback uses)?
tbh try the arguments injection based approach
the idea is you have an additional field called args, a reference to an associated type
then for DynTaskFrame you just add an additional generic
like what happen if user use FallbackTaskFrame as 2nd frame in workflow ?
and when scheduling / building a Task, the TaskFrame would need to have arguments of ()
this is where the tricky part comes in
FallbackTaskFrame would need to tell the first TaskFrame it accepts no arguments, then the secondary has to accept as argument the error type of the primary
and
consider the fact the secondary TaskFrame may not be direct
like say a RetriableTaskFrame may be present
or even hell another FallbackTaskFrame like you said
one idea is to make the arguments recursive, basically
Initially: ()
1st Fallback: (Args, Err1)
2nd Fallback: (Args, Err2) -> ((Args, Err1), Err2)
the other TaskFrames woudl transport the arguments
hmm okay, but need to add docs asap
@placid drum honestly, if you can try, i'd suggest better do this issue
okay
i'll try my best
howz the progress going?
going well hard to move in repo but ya understanding lot things
wdym hard to move in repo, hm?
This is now over https://github.com/GitBrincie212/ChronoGrapher/issues/149
i rewrote the issues
wrong issue
ok now correct
How do I put it in word's? Anyway it's like I don't know whole codebase so making changes is little difficult
@fervent lark check pr if any suggestion please let me know.
ok
its ok
quite messy it seems
also why clone the arguments?
you need pass by reference
for fallback
for dynamictaskframe it should require the arguments
do wish it was a bit more ergonomic
macros i guess will help out with this one
okay i try to make it little bit ergonomic
yea sure
for fallback, you can just require the argument of the second TaskFrame to be (&T::Args, ...)
so it would then be
&(&T::Args, ...)
not the best but definitely more performant and less restrictive
okay
@fervent lark adding refrence will cause lot changes regrading lifetime should i do that ?
every TaskFrame
ye
you probably should
its gonna be difficult
but do aim for it + making it more ergonomic
okay i try my best
for the macro, i do imagine it would automatically unwrap the arguments from this tuple (sort of like being varargs)
but
thats just for the macros
okay
howz progress going?
its kicking my a** kinda hard
well lets see till tomorow how much i can do it
ye predicted me
i am noob for now.
eh its a hard issue even for me
@fervent lark i need help so right now, erased task cant use dyn taskframe due GAT, should i remove dynamic task frame we use &dyn Any so do dynamic dispatch is possible.
why remove it though?
oh
#[async_trait]
pub trait TaskFrame: 'static + Send + Sync + Sized {
type Error: TaskError;
async fn execute(&self, ctx: &TaskFrameContext) -> Result<(), Self::Error>;
}
Thats not GAT btw, just an associated type
also the Sized requirement prohibits you to use object safe code
plus i want to keep this kind of interface
also for &dyn Any, well... downcasting would slow us down a bit, so we have to use some unsafe code
btw for the handle-based approach, apparently we can't remove the SchedulerHandle
its a part thats needed
oh okay
#[async_trait]
pub trait TaskFrame: 'static + Send + Sync + Sized {
type Error: TaskError;
type Args<'a> : Send + Sync;
async fn execute<'a>(&self, ctx: &TaskFrameContext, args: &Self::Args<'a >) -> Result<(), Self::Error>;
}
i was using this approach
oh
you can unelide the lifetimes though?
#[async_trait]
pub trait TaskFrame: 'static + Send + Sync + Sized {
type Error: TaskError;
type Args<'a> : Send + Sync;
async fn execute(&self, ctx: &TaskFrameContext, args: &Self::Args) -> Result<(), Self::Error>;
}
im pretty sure like that
or if you must use the placeholder lifetime then just add <'_>
i might be saying bullshit
it might, just might be possible to remove it
though only for like the TaskFrame side, the TaskHook side is something more difficult
we could take advantage of unsafe code and have it call the TaskHandle indirectly
@fervent lark
impl has stricter requirements than trait
impl has extra requirement `'a: 'async_trait`
#[async_trait]
pub trait TaskFrame: 'static + Send + Sync {
type Error: TaskError;
type Args<'a>: Send + Sync;
async fn execute(
&self,
ctx: &TaskFrameContext,
args: &Self::Args<'_>,
) -> Result<(), Self::Error>;
}
#[async_trait]
pub trait DynTaskFrame<'a, E: TaskError, A: Send + Sync + 'a>: 'static + Send + Sync {
async fn erased_execute(&self, ctx: &TaskFrameContext, args: &A) -> Result<(), E>;
fn erased(&self) -> &dyn ErasedTaskFrame;
}
#[async_trait]
impl<'a, T: TaskFrame> DynTaskFrame<'a, T::Error, T::Args<'a>> for T
where
T::Args<'a>: Send + Sync + 'a,
{
async fn erased_execute(
&self,
ctx: &TaskFrameContext,
args: &T::Args<'a>,
) -> Result<(), T::Error> {
self.execute(ctx, args).await
}
fn erased(&self) -> &dyn ErasedTaskFrame {
self
}
}

a lot of lifetime hell as it seems
ye try to avoid lots of named lifetimes as much as possible
if its annonymous / placeholder then its ok
but this just gets out of hand
ya
if we remove async trait it would work but will need to write future by hand
wtf i am suggesting lol

any suggetion ?
tbh idk
im trying to do the TaskHandle approach
its difficult af, but the more i analyze it the more i realize it is needed
take single-node persistence for example, TaskHooks are in a global registry which means its impossible to persist them, no easy way to inspect the structure of Tasks or let the SchedulerTaskStore add its own metadata on top of the Tasks without using TaskHooks
with the handle-based approach, storage is more flexible at the cost of more complexity ofc but definitely worth it
ya its worth it, regardless of complexity.
one of the problems will be how to make TaskFrameContext and TaskHookContext allow communication regardless if a TaskHandle or Task<T1, T2> is used
my mind is being f_cked by the complexity of TaskHandles
btw the more i see it the more better it may be to simplify both context objects, removing all of their data
the only data they would hold is the handle
honestly its best to leave TaskHandles
and just focus on shipping the product in an alpha stage
Yes ig
@fervent lark should i keep working on that or should i write doc ?
honestly its something i want it gone as an issue
try to work on it
if we get over with it
i can continue the guidebook documentation
this thing keeps me off from continuing due to its unknown shape
okay
look try your best in order to get this issue done
yup
man i wish we moved more quickly
i think this rocks, i've edesigned a bit the logo
nvm i nailed it
i think i cooked
this is more for marketing side of things, the logo will be simplified for say the website (since its quite complex)
final logo
im wishing we get more help
like we're capped in this size of a team
and we move at a snail pace
though gotta admit, absolute cinema, we reached 31 github stars
though i'd argue help is more important
jesus fuck i just woke up and now its like 39 stars, kinda impressive
yet... No help which the one thing i need most rn
you will get
the problem is when
help should arrive soon since thats the critical stage
if it arrives towards the completion of the project, then its kinda useless
well you are right about this.
45
Soon 100
@fervent lark about 141 issue what kind of ergonomic you're looking for? Like can you give example it would be nice to have reference.
give a sec
okay
for https://github.com/GitBrincie212/ChronoGrapher/issues/141 tbh i don't have in mind though i do want the use to be simple in the sense:
#[async_trait]
impl TaskFrame for MyTaskFrame {
type Error = ...;
type Args = ...; // Maybe use GATs for lifetimes and stuff
async fn execute(&self, ctx: &TaskFrameContext, args: &Self::Args) -> Result<(), Self::Error> {
// ...
}
}
did you checked my last pr ?
i am thinking of making it static what do you think
#[async_trait]
pub trait TaskFrame: 'static + Send + Sync + Sized {
type Error: TaskError;
type Args<'a>: Send + Sync;
async fn execute(&self, ctx: &TaskFrameContext, args: &Self::Args<'_>) -> Result<(), Self::Error>;
}
#[async_trait]
impl<T: TaskFrame<Error: Into<T::Error>>> DynTaskFrame<T::Error, T::Args<'static>> for T {
async fn erased_execute(&self, ctx: &TaskFrameContext, args: &(T::Args<'static>)) -> Result<(), T::Error> {
self.execute(ctx, args).await
}
fn erased(&self) -> &dyn ErasedTaskFrame {
self
}
}
well the lifetime of execute method
and so that we dont need to clone ?
hmm make sense
kind of starting to feel your pain though, i've attempted to do this myself just in case i crack it
lifetime parameters or bounds on method erased_execute do not match the trait declaration [E0195]
lifetimes do not match method in trait
i dont have clear picture like if it have life time 2nd execute from task frame isnt it will drop when moving to fallback frame ?
?
i guess i will become mad someday
lol
one way to solve this is via Arc<T> but this is just disguisting
both ergonomically and performance-wise
Ya arc will add extra overhead
Can't we just pass memory address 
It will need lifetime so no memory sharing
hell no
thats basically asking for a UB to happen
btw im going to strip down the information the context gives
Why what's on your mind?
they are quite useless
Hmm I read about Kafka architecture yesterday they use offset not any I'd to track the messages
Can't we do same for task it will same extra overhead?
Like we give option to put I'd for task, instead of that can't we create in serial number as task. If user needed he can use that to get task
error[E0599]: no method named into_erased found for struct DynamicTaskFrame<T> in the current scope
--> tests/src/task/frames/dynamic_taskframe_test.rs:35:11
|
35 | frame.into_erased().run().await?;
Is it normal ?
It's error so, it should not be normal
hmmm... So for example
[A][B][C][D]
We want D, instead of having a reference or anything like that we just use an offset of 3
i don't think i get it
Hmm it will be same ig Ahh
But let me explain
So what kafka does it store via offset. If I want 5 no offest. Consumer will try to get it.
Here task will store with offset and if we certain offset something will try to get it
you gotta fix your spelling man, its kinda hard to understand
Yes I am also not able to express properly
I'll improve
Also should I do that issue with static?
hold on il committ some changes
done
Ok I'll do and add pr
This is a mindfuck on so many levels
i want simplicity, like the execution tree it should feel like stacking LEGO on top of each other while being familiar in typical programming, no like DAGs in the mix and this is really hard
@placid drum
ig do docs
this shit is too complex
focus on scheduler stuff for now
its not PR
its in the commits
basically one of the sacrafices i had to do is:
impl<T, T2> TaskFrame for FallbackTaskFrame<T, T2>
where
T: TaskFrame,
T2: TaskFrame<Args = T::Error>
{
type Error = T2::Error;
type Args = T::Args;
async fn execute(&self, ctx: &TaskFrameContext, args: &Self::Args) -> Result<(), Self::Error> {
// ...
}
}
no additional Args parameter
but...
you can get around this limitation, via a middleman that attaches this argument type as a TaskHook more specifically a shared object
dynamic? Yes, does it happens too often to worry about? Definitely no
i've realized something...
ye the clones...

this doesn't work
struct TaskFrameA;
struct TaskFrameB;
impl TaskFrame for TaskFrameA {
type Error = String;
type Args = ();
async fn execute(&self, _ctx: &TaskFrameContext, _args: &Self::Args) -> Result<(), Self::Error> {
Err("error".to_string())
}
}
impl TaskFrame for TaskFrameB {
type Error = String;
type Args = String;
async fn execute(&self, _ctx: &TaskFrameContext, args: &Self::Args) -> Result<(), Self::Error> {
Err(*args) // We are taking String by reference
}
}
end me...
im done, this is the user's choice
not mine
either make the type cloneable or use an Arc<T>
nothing else
feeling the heat
git so fckin hard 
haa if we do x post 100 star easy
lol
ig do it though yourself
just remember not to share it prematurely as otherwise we won't maximize the effect and may damage our image
seems to
commits endpoint works well so this should work
also the guidebook's workflows chapter is slowly being finished
the funny thing is the Commits button works
il have to finish the Dependencies chapter
tweak "Time In A Wokflow" a bit
and add a Patterns & Summary chapter summarizing everything i taught
wth 😂
Waiting 49ms
thread 'task::frames::timeout_taskframe_test::task_finishing_after_timeout_returns_error' (532316) panicked at tests/src/task/frames/timeout_taskframe_test.rs:62:5:
assertion failed: exec.is_err()
TimeoutTaskFrame is precise at less than a millisecond 😮
have you not considered this is litterally a simple timer and uses the OS which is quite precise plus no overhead such as NTP which could throw precision 
I did not even look at the timer impl 
Hey I would like to focus on cron! Macro. I have few questions tho. Sorry if they may sound dumb or obvious to You - I’m yet to understand the repo 🙂
I digged a little bit through the repo.
The docstrings mention that cron! already exist and I found some stub only - I assume it’s just assuming that the pending issue as already finalized ?
I found the dynamic logic here and I assume I may use/base heavily on those
https://github.com/GitBrincie212/ChronoGrapher/blob/master/base/src/task/trigger/schedule/cron.rs
As far as I understand it, dod of the macro is:
- Validate the input literals and raise compiler error if those fail
- Construct TaskScheduleCron from multiple cron fields input literals
- Add unit tests
docstrings assumes that some features are already there if im not wrong
ask McBrincie212 he will enlighten you
Its ok, i get you're new to the project, no one fully knows everything (except me since i provision the project).
The docstring yes, they assume the feature is finalized (as to not have to update it continiously and only under changing the feature). The cron! macro is not implemented but the dynamic logic is already done. I would advise refining the dynamic logic since its not the best so you can extract it more easily
All of the bullets of the dod are true and exactly what i want. However, do not reimplement the cron parser, rather find a way to exract the parser and transpiler in such a way that its used in the dynamic logic and the macro logic to reduce mantainability cost
Understood, thanks for clarification
@fervent lark loved they frame dependency work like you can disable but even its resolved this will treated as unresolved by scheduler.
without changing dependency graph
thx ig
Well that seems small but make lot difference
I never used other complex cron so don't know it's common feature or not
disabling dependencies isn't really common
.