For version 0.8.0:
------------------
* Get rid of "floating" service state:
- - soft dependencies should always be re-attached when the dependency starts.
- - "dinitctl wake" should fail (report error) if there are no running dependents.
+ - soft dependencies should remain attached when a dependency restarts and removed
+ only if it stops without restarting
+ - "dinitctl wake" should fail (report error) if there are no running dependents and
+ should re-attach soft dependencies.
- if a service starts and required-by count is 0 it should always immediately stop.
- update man pages accordingly.
* Easy way to reload service description (including if service is running, where possible).
void base_process_service::emergency_stop() noexcept
{
- if (! do_auto_restart() && start_explicit) {
- start_explicit = false;
- release(false);
- }
forced_stop();
stop_dependents();
}
}
// 1 byte: packet type
- // 1 byte: flags eg pin in requested state (0 = no pin, 1 = pin)
+ // 1 byte: flags eg. pin in requested state (0 = no pin, 1 = pin)
// 4 bytes: service handle
bool do_pin = ((rbuf[1] & 1) == 1);
// force service to stop
bool gentle = ((rbuf[1] & 2) == 2);
bool do_restart = ((rbuf[1] & 4) == 4);
- bool is_active = service->is_marked_active();
if (do_restart && services->is_shutting_down()) {
ack_buf[0] = DINIT_RP_NAK;
break;
goto clear_out;
}
}
- if (do_pin) service->pin_stop();
- service->stop(true);
- service->forced_stop();
- services->process_queues();
- service_state_t wanted_state = service_state_t::STOPPED;
+ service_state_t wanted_state;
if (do_restart) {
- service->start(is_active);
+ service->restart(); // TODO XXX check return, reply NAK if fail
wanted_state = service_state_t::STARTED;
- services->process_queues();
}
+ else {
+ if (do_pin) service->pin_stop();
+ service->stop(true);
+ wanted_state = service_state_t::STOPPED;
+ }
+ service->forced_stop();
+ services->process_queues();
if (service->get_state() == wanted_state) ack_buf[0] = DINIT_RP_ALREADYSS;
break;
}
const dependency_type dep_type;
+ // Check if the dependency is a hard dependency (including milestone still waiting).
+ bool is_hard()
+ {
+ return dep_type == dependency_type::REGULAR
+ || (dep_type == dependency_type::MILESTONE && waiting_on);
+ }
+
service_dep(service_record * from, service_record * to, dependency_type dep_type_p) noexcept
: from(from), to(to), waiting_on(false), holding_acq(false), dep_type(dep_type_p)
{ }
bool prop_start : 1;
bool prop_stop : 1;
- bool restarting : 1; // re-starting after unexpected termination
+ bool restarting : 1; // re-start after stopping
bool start_failed : 1; // failed to start (reset when begins starting)
bool start_skipped : 1; // start was skipped by interrupt
// Release console (console must be currently held by this service)
void release_console() noexcept;
- bool do_auto_restart() noexcept;
-
// Started state reached
bool process_started() noexcept;
void start(bool activate = true) noexcept; // start the service
void stop(bool bring_down = true) noexcept; // stop the service
+ void restart() noexcept; // restart the service
void forced_stop() noexcept; // force-stop this service and all dependents
auto next = prop_queue.pop_front();
next->do_propagation();
}
- while (! stop_queue.is_empty()) {
+ if (! stop_queue.is_empty()) {
auto next = stop_queue.pop_front();
next->execute_transition();
}
{
bool did_exit = exit_status.did_exit();
bool was_signalled = exit_status.was_signalled();
- restarting = false;
auto service_state = get_state();
if (notification_fd != -1) {
// This may be a "smooth recovery" where we are restarting the process while leaving the
// service in the STARTED state.
if (restarting && service_state == service_state_t::STARTED) {
- restarting = false;
+ //restarting = false;
bool need_stop = false;
if ((did_exit && exit_status.get_exit_status() != 0) || was_signalled) {
need_stop = true;
return;
}
- restarting = false;
+ //restarting = false;
if (service_state == service_state_t::STARTING) {
// POSIX requires that if the process exited clearly with a status code of 0,
// the exit status value will be 0:
do_smooth_recovery();
return;
}
- if (! do_auto_restart() && start_explicit) {
- start_explicit = false;
- release(false);
- }
stop_reason = stopped_reason_t::TERMINATED;
forced_stop();
stop_dependents();
force_stop = false;
- bool will_restart = (desired_state == service_state_t::STARTED)
- && !services->is_shutting_down();
+ restarting |= auto_restart;
+ bool will_restart = restarting && required_by > 0;
+ restarting = false;
+
+ // If we won't restart, break soft dependencies now
+ if (! will_restart) {
+ for (auto dept : dependents) {
+ if (! dept->is_hard()) {
+ // waits-for or soft dependency:
+ if (dept->waiting_on) {
+ dept->waiting_on = false;
+ dept->get_from()->dependency_started();
+ }
+ if (dept->holding_acq) {
+ dept->holding_acq = false;
+ // release without issuing stop, since we're called only when this
+ // service is already stopped/stopping:
+ release(false);
+ }
+ }
+ }
+ }
for (auto & dependency : depends_on) {
// we signal dependencies in case they are waiting for us to stop:
notify_listeners(service_event_t::STOPPED);
}
-bool service_record::do_auto_restart() noexcept
-{
- if (auto_restart) {
- return !services->is_shutting_down();
- }
- return false;
-}
-
void service_record::require() noexcept
{
if (required_by++ == 0) {
// Can stop, and can release dependencies now. We don't need to issue a release if
// the require was pending though:
- prop_release = !prop_require;
- prop_require = false;
- services->add_prop_queue(this);
+ if (service_state != service_state_t::STOPPED && service_state != service_state_t::STOPPING) {
+ prop_release = !prop_require;
+ prop_require = false;
+ services->add_prop_queue(this);
+ }
if (service_state == service_state_t::STOPPED) {
services->service_inactive(this);
require();
start_explicit = true;
}
-
- if (desired_state == service_state_t::STARTED && service_state != service_state_t::STOPPED) return;
bool was_active = service_state != service_state_t::STOPPED || desired_state != service_state_t::STOPPED;
desired_state = service_state_t::STARTED;
if (service_state != service_state_t::STOPPED) {
// We're already starting/started, or we are stopping and need to wait for
// that the complete.
- if (service_state != service_state_t::STOPPING || ! can_interrupt_stop()) {
+ if (service_state != service_state_t::STOPPING) {
return;
}
+
+ if (! can_interrupt_stop()) {
+ restarting = true;
+ return;
+ }
+
// We're STOPPING, and that can be interrupted. Our dependencies might be STOPPING,
// but if so they are waiting (for us), so they too can be instantly returned to
// STARTING state.
else if (service_state == service_state_t::STOPPING) {
if (stop_check_dependents()) {
waiting_for_deps = false;
+
+ // A service that does actually stop for any reason should have its explicit activation released, unless
+ // it will restart:
+ if (start_explicit && !auto_restart && !restarting) {
+ start_explicit = false;
+ release(false);
+ }
+
bring_down();
}
}
}
bool start_success = bring_up();
+ restarting = false;
if (! start_success) {
failed_to_start();
}
{
if (start_explicit) {
start_explicit = false;
- release();
+ required_by--;
}
// If our required_by count is 0, we should treat this as a full manual stop regardless
- if (required_by == 0) bring_down = true;
-
- // If it's a manual bring-down, we'll also break holds from waits-for dependencies, to avoid
- // bouncing back up again -- but only if all holds are from waits-for dependencies.
- if (bring_down) {
- if (std::all_of(dependents.begin(), dependents.end(), [](service_dep * x) {
- return x->dep_type == dependency_type::WAITS_FOR || x->dep_type == dependency_type::SOFT; }))
- {
- for (auto dept : dependents) {
- if (dept->waiting_on) {
- dept->waiting_on = false;
- dept->get_from()->dependency_started();
- }
- if (dept->holding_acq) {
- dept->holding_acq = false;
- // release without issuing stop, since we issue stop if necessary below
- release(false);
- }
- }
- }
+ if (required_by == 0) {
+ bring_down = true;
}
if (bring_down && service_state != service_state_t::STOPPED
}
}
-void service_record::do_stop() noexcept
+void service_record::restart() noexcept
{
- // A service that does actually stop for any reason should have its explicit activation released, unless
- // it will restart:
- if (start_explicit && ! do_auto_restart()) {
- start_explicit = false;
- release(false);
+ // Re-start without affecting dependency links/activation.
+
+ if (service_state == service_state_t::STARTED) {
+ restarting = true;
+ stop_reason = stopped_reason_t::NORMAL;
+ do_stop();
}
+}
+
+void service_record::do_stop() noexcept
+{
+ // Called when we should definitely stop. We may need to restart afterwards, but we
+ // won't know that for sure until the execution transition.
bool all_deps_stopped = stop_dependents();
if (pinned_started) return;
+ if (required_by == 0) {
+ prop_release = true;
+ services->add_prop_queue(this);
+ }
+
service_state = service_state_t::STOPPING;
waiting_for_deps = true;
if (all_deps_stopped) {
{
bool all_deps_stopped = true;
for (auto dept : dependents) {
- if (dept->dep_type == dependency_type::REGULAR && ! dept->get_from()->is_stopped()) {
+ if (dept->is_hard() && dept->holding_acq) {
all_deps_stopped = false;
break;
}
}
-
+
return all_deps_stopped;
}
{
bool all_deps_stopped = true;
for (auto dept : dependents) {
- if (dept->dep_type == dependency_type::REGULAR ||
- (dept->dep_type == dependency_type::MILESTONE &&
- dept->get_from()->service_state != service_state_t::STARTED)) {
+ if (dept->is_hard() && dept->holding_acq) {
if (! dept->get_from()->is_stopped()) {
// Note we check *first* since if the dependent service is not stopped,
// 1. We will issue a stop to it shortly and
dept->get_from()->prop_stop = true;
services->add_prop_queue(dept->get_from());
}
- else {
- // waits-for or soft dependency:
- if (dept->waiting_on) {
- dept->waiting_on = false;
- dept->get_from()->dependency_started();
- }
- if (dept->holding_acq && !auto_restart) {
- dept->holding_acq = false;
- // release without issuing stop, since we should be called only when this
- // service is already stopped/stopping:
- release(false);
- }
- }
}
return all_deps_stopped;
{
if (pinned_started) {
pinned_started = false;
+
+ for (auto &dep : depends_on) {
+ if (dep.is_hard()) {
+ if (dep.get_to()->get_state() != service_state_t::STARTED) {
+ desired_state = service_state_t::STOPPED;
+ }
+ }
+ else if (dep.holding_acq) {
+ dep.holding_acq = false;
+ dep.get_to()->release();
+ }
+ }
+
if (desired_state == service_state_t::STOPPED || force_stop) {
do_stop();
services->process_queues();
assert(p.get_state() == service_state_t::STARTED);
base_process_service_test::handle_exit(&p, 0);
- sset.process_queues();
assert(p.get_state() == service_state_t::STOPPED);
assert(p.get_stop_reason() == stopped_reason_t::TERMINATED);
assert(tp.get_state() == service_state_t::STARTING);
assert(p.get_state() == service_state_t::STOPPING);
+ // p terminates (finishes stopping). Then it should re-start...
base_process_service_test::handle_signal_exit(&p, SIGTERM);
sset.process_queues();
// s3 should remain started due to pin:
assert(s3->get_state() == service_state_t::STARTED);
assert(s2->get_state() == service_state_t::STOPPING);
- assert(s1->get_state() == service_state_t::STOPPING);
+ assert(s1->get_state() == service_state_t::STARTED);
// If we now unpin, s3 should stop:
s3->unpin();
assert(s1->get_state() == service_state_t::STOPPED);
}
-// Test that STOPPING dependency of pinned service returns to STARTED when the pinned service has
-// a start issued.
+// Test that issuing a stop to a pinned-started service does not stop the service or its dependencies.
void test_pin2()
{
service_set sset;
s3->stop(true);
sset.process_queues();
- // s3 should remain started due to pin, but s1 and s2 are released and go STOPPING:
- assert(s3->get_state() == service_state_t::STARTED);
- assert(s2->get_state() == service_state_t::STOPPING);
- assert(s1->get_state() == service_state_t::STOPPING);
-
- // If we now issue start, STOPPING should revert to STARTED:
- s3->start(true);
- sset.process_queues();
-
+ // s3 should remain started due to pin, s1 and s2 not released:
assert(s3->get_state() == service_state_t::STARTED);
assert(s2->get_state() == service_state_t::STARTED);
assert(s1->get_state() == service_state_t::STARTED);
// s3 should remain started due to pin, but s1 and s2 are released and go STOPPING:
assert(s3->get_state() == service_state_t::STARTED);
assert(s2->get_state() == service_state_t::STOPPING);
- assert(s1->get_state() == service_state_t::STOPPING);
+ assert(s1->get_state() == service_state_t::STARTED);
// If we now issue start, s2 still needs to stop (due to force stop):
s3->start(true);
service_record *s1 = new service_record(&sset, "test-service-1", service_type_t::INTERNAL, {});
sset.add_service(s1);
- // Pin s3:
+ // Pin s1:
s1->pin_start();
// Start the service:
assert(s1->get_state() == service_state_t::STARTED);
- // Issue stop:
+ // Issue forced stop:
s1->stop(true);
+ s1->forced_stop();
sset.process_queues();
// s3 should remain started:
assert(sset.count_active_services() == 0);
}
+// Tests for "restart" functionality.
+void test13()
+{
+ service_set sset;
+
+ test_service *s1 = new test_service(&sset, "test-service-1", service_type_t::INTERNAL, {});
+ test_service *s2 = new test_service(&sset, "test-service-2", service_type_t::INTERNAL, {{s1, WAITS}});
+ test_service *s3 = new test_service(&sset, "test-service-3", service_type_t::INTERNAL, {{s2, REG}});
+
+ sset.add_service(s1);
+ sset.add_service(s2);
+ sset.add_service(s3);
+
+ // Start all services via s3
+ sset.start_service(s3);
+ s1->started();
+ sset.process_queues();
+ s2->started();
+ sset.process_queues();
+ s3->started();
+ sset.process_queues();
+
+ assert(s3->get_state() == service_state_t::STARTED);
+ assert(s2->get_state() == service_state_t::STARTED);
+ assert(s1->get_state() == service_state_t::STARTED);
+
+ s1->restart();
+ s1->forced_stop();
+ sset.process_queues();
+
+ assert(s3->get_state() == service_state_t::STARTED);
+ assert(s2->get_state() == service_state_t::STARTED);
+ assert(s1->get_state() == service_state_t::STARTING);
+
+ s1->started();
+ sset.process_queues();
+
+ assert(s3->get_state() == service_state_t::STARTED);
+ assert(s2->get_state() == service_state_t::STARTED);
+ assert(s1->get_state() == service_state_t::STARTED);
+}
+
+// Make sure a service only restarts once (i.e. restart flag doesn't get stuck)
+void test14()
+{
+ service_set sset;
+
+ test_service *s1 = new test_service(&sset, "test-service-1", service_type_t::INTERNAL, {});
+ test_service *s2 = new test_service(&sset, "test-service-2", service_type_t::INTERNAL, {{s1, WAITS}});
+
+ sset.add_service(s1);
+ sset.add_service(s2);
+
+ // Start all services via s3
+ sset.start_service(s2);
+ s1->started();
+ sset.process_queues();
+ s2->started();
+ sset.process_queues();
+
+ assert(s2->get_state() == service_state_t::STARTED);
+ assert(s1->get_state() == service_state_t::STARTED);
+
+ s1->restart();
+ s1->forced_stop();
+ sset.process_queues();
+
+ assert(s2->get_state() == service_state_t::STARTED);
+ assert(s1->get_state() == service_state_t::STARTING);
+
+ s1->started();
+ sset.process_queues();
+
+ assert(s2->get_state() == service_state_t::STARTED);
+ assert(s1->get_state() == service_state_t::STARTED);
+
+ // Ok, we restarted s1. Now stop it:
+
+ s1->stop(true);
+ sset.process_queues();
+
+ assert(s2->get_state() == service_state_t::STARTED);
+ assert(s1->get_state() == service_state_t::STOPPED); // didn't restart
+}
+
#define RUN_TEST(name, spacing) \
std::cout << #name "..." spacing << std::flush; \
RUN_TEST(test10, " ");
RUN_TEST(test11, " ");
RUN_TEST(test12, " ");
+ RUN_TEST(test13, " ");
+ RUN_TEST(test14, " ");
}