-
-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature] DynamicSupervisor please #42
Comments
Sounds great! |
I'm also interested in this. In terms of API design, do you see any value in having a separate supervisor and dynamic supervisor (afaict this is the case in Elixir) or should there just be a single supervisor type that happens to be dynamic, if you want it to be static you don't add more children to it. I'm personally leaning towards the option of just having a single type that is dynamic. Also, should the supervisor still have an init function to setup initial children or do you just create a supervisor and start adding children to it dynamically? Option 1 pub fn main() {
let assert Ok(sup) = supervisor.start(fn(children) {
children
|> add(worker(database.start))
|> add(worker(monitoring.start))
|> add(worker(web.start))
})
// Something happens in between
let assert Ok(runner) = supervisor.add_child(sup, worker(runner.start))
} Option 2 pub fn main() {
let assert Ok(sup) = supervisor.start()
let assert Ok(db) = supervisor.add_child(sup, worker(database.start))
let assert Ok(mon) = supervisor.add_child(sup, worker(monitoring.start))
let assert Ok(web) = supervisor.add_child(sup, worker(web.start))
// Something happens in between
let assert Ok(runner) = supervisor.add_child(sup, worker(runner.start))
} I see the value in not breaking the existing API but I also find option 2 to be a bit simpler API. |
With option 2 how does it restart the children when one dies? |
I haven't really looked into the current implementation but I'm assuming it will need to keep some kind of list of children. Does it currently call the init function every time a child dies? |
Nope, the current supervisor implements the |
Oh I see, to be honest I wasn't too familiar with all the different strategies. I guess So some thoughts:
Or would it be preferred to have distinct static and dynamic supervisors? |
I don't think we could safely restart any dynamically added children as the initial state that was used to create them is not controlled by the supervisor. Take a web server that does some background processing as an example. It could have a web server process, a database, connection process, and dynamically, added worker processes. If there was to be a failure, which caused them all to be restarted, the web application and the database, connection processes would be initialise correctly, but if any of the work processes were restarted using their original initial state, they would have references to the no longer existing database connection process, and such would always fail. This would eventually result in there being too much restart intensity and the entire supervisor would fail. |
I see. Given my limited experience using supervisors I might not be the best person to come up with designs for this 😅. Would a more typical use case add the dynamic supervisor as a child of a static one so that the whole dynamic supervisor is restarted if any of its dependencies crash (web server, database, ...)? If so, then two distinct types of supervisors might make more sense. |
I definitely could make use of a dynamic supervisor. From what I've read, my understanding is that the For a dynamic supervisor though, I feel like it'd be easier to use a Am I understanding the situation correctly ? |
Yup that's right. "Dynamic supervisor" is what Elixir folks call a supervisor with the one for one strategy. |
I've been thinking about how to implement a dynamic supervisor. I'm trying to start from the current otp
Here's an overview: pub opaque type Message(argument, children_message) {
ChildAdded(child: ChildSpec(children_message, argument))
Exit(process.ExitMessage)
RetryRestart(Pid)
}
type State {
State(
restarts: IntensityTracker,
// starter: Starter(a),
retry_restarts: Subject(Pid),
)
}
pub type Spec(argument) {
Spec(
argument: argument,
max_frequency: Int,
frequency_period: Int,
init: fn(argument) -> Nil,
)
}
pub opaque type ChildSpec(msg, argument) {
ChildSpec(
start: fn(argument) -> Result(Subject(msg), StartError),
finalize: fn(argument, Subject(msg)) -> Nil,
)
}
fn loop(
message: Message(argument, children_message),
state: State,
) -> actor.Next(Message(argument, children_message), State) {
case message {
Exit(exit_message) -> todo //handle_exit(exit_message.pid, state)
RetryRestart(pid) -> todo //handle_exit(pid, state)
ChildAdded(child) -> todo
}
}
fn init(
spec: Spec(argument),
) -> actor.InitResult(State, Message(argument, children_message)) {
// Create a subject so that we can asynchronously retry restarting when we
// fail to bring an exited child
let retry = process.new_subject()
// Trap exits so that we get a message when a child crashes
process.trap_exits(True)
// Combine selectors
let selector =
process.new_selector()
|> process.selecting(retry, RetryRestart)
|> process.selecting_trapped_exits(Exit)
todo
}
pub fn worker(
start: fn(argument) -> Result(Subject(msg), StartError),
finalize: fn(argument, Subject(msg)) -> Nil,
) -> ChildSpec(msg, argument) {
ChildSpec(start: start, finalize: finalize)
} One issue with this design is all the workers under the same dynamic supervisor must all return a subject with the same message type. IDK if this is the same in Elixir, but I was forced to specify the |
I think the correct way to go would be to use the Erlang supervisor module as a base as it is well tested and known to work. That's what That said, I've not looked into this much so I don't know what the API might be or what limitations might arise. |
I see. |
I started working on the
I essentially have a functional (but messy) dynamic supervisor using the Erlang I haven't opened a PR yet because I think my work is very, very messy, and I'd like some feedback first. The |
I made a usage looks like this: import gleam/otp/dynamic_supervisor as sup
pub fn start_supervisor() {
let assert Ok(supervisor) =
sup.new(sup.worker_child("worker", start_worker))
|> sup.start_link
let assert Ok(p1) = sup.start_child(supervisor, "1")
let assert Ok(p2) = sup.start_child(supervisor, "2")
}
fn start_worker(args) -> Result(Pid, error) {
// Start a worker process
} If this feels like an appropriate addition, I'd be happy to open a PR. |
@ryanmiville Great minds think alike it seems. I opened a PR with a very similar take a few weeks ago. Still discussing with Louis regarding how to organize the API. |
There's not a discussion! I have been clear about what the design must be for a contribution to be accepted into this library. |
Hello.
I think Gleam's typed OTP and its concept of subject is great.
But, It would be better to have some types of DynamicSupervisor that allow me to spawn workers as I need.
The text was updated successfully, but these errors were encountered: