Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions include/envoy/init/init.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ namespace Envoy {
namespace Init {

/**
* A single initialization target.
* A single initialization target. Deprecated, use SafeInit::Target instead.
* TODO(mergeconflict): convert all Init::Target implementations to SafeInit::TargetImpl.
*/
class Target {
public:
Expand All @@ -25,7 +26,8 @@ class Target {
};

/**
* A manager that initializes multiple targets.
* A manager that initializes multiple targets. Deprecated, use SafeInit::Manager instead.
* TODO(mergeconflict): convert all Init::Manager uses to SafeInit::Manager.
*/
class Manager {
public:
Expand Down
31 changes: 31 additions & 0 deletions include/envoy/safe_init/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
licenses(["notice"]) # Apache 2

load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_library",
"envoy_package",
)

envoy_package()

envoy_cc_library(
name = "watcher_interface",
hdrs = ["watcher.h"],
)

envoy_cc_library(
name = "target_interface",
hdrs = ["target.h"],
deps = [
":watcher_interface",
],
)

envoy_cc_library(
name = "manager_interface",
hdrs = ["manager.h"],
deps = [
":target_interface",
":watcher_interface",
],
)
79 changes: 79 additions & 0 deletions include/envoy/safe_init/manager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#pragma once

#include "envoy/common/pure.h"
#include "envoy/safe_init/target.h"
#include "envoy/safe_init/watcher.h"

namespace Envoy {
namespace SafeInit {

/**
* SafeInit::Manager coordinates initialization of one or more "targets." A typical flow would be:
*
* - One or more initialization targets are registered with a manager using `add`.
* - The manager is told to `initialize` all its targets, given a Watcher to notify when all
* registered targets are initialized.
* - Each target will initialize, either immediately or asynchronously, and will signal
* `ready` to the manager when initialized.
* - When all targets are initialized, the manager signals `ready` to the watcher it was given
* previously.
*
* Since there are several entities involved in this flow -- the owner of the manager, the targets
* registered with the manager, and the manager itself -- it may be difficult or impossible in some
* cases to guarantee that their lifetimes line up correctly to avoid use-after-free errors. The
* interface design here in SafeInit allows implementations to avoid the issue:
*
* - A Target can only be initialized via a TargetHandle, which acts as a weak reference.
* Attempting to initialize a destroyed Target via its handle has no ill effects.
* - Likewise, a Watcher can only be notified that initialization was complete via a
* WatcherHandle, which acts as a weak reference as well.
*
* See target.h and watcher.h, as well as implementation in source/common/safe_init for details.
*/
struct Manager {
virtual ~Manager() = default;

/**
* The manager's state, used e.g. for reporting in the admin server.
*/
enum class State {
/**
* Targets have not been initialized.
*/
Uninitialized,
/**
* Targets are currently being initialized.
*/
Initializing,
/**
* All targets have been initialized.
*/
Initialized
};

/**
* @return the current state of the manager.
*/
virtual State state() const PURE;

/**
* Register an initialization target. If the manager's current state is uninitialized, the target
* will be saved for invocation later, when `initialize` is called. If the current state is
* initializing, the target will be invoked immediately. It is an error to register a target with
* a manager that is already in initialized state.
* @param target the target to be invoked when initialization begins.
*/
virtual void add(const Target& target) PURE;

/**
* Start initialization of all previously registered targets, and notify the given Watcher when
* initialization is complete. It is an error to call initialize on a manager that is already in
* initializing or initialized state. If the manager contains no targets, initialization completes
* immediately.
* @param watcher the watcher to notify when initialization is complete.
*/
virtual void initialize(const Watcher& watcher) PURE;
};

} // namespace SafeInit
} // namespace Envoy
52 changes: 52 additions & 0 deletions include/envoy/safe_init/target.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#pragma once

#include <memory>

#include "envoy/common/pure.h"
#include "envoy/safe_init/watcher.h"

#include "absl/strings/string_view.h"

namespace Envoy {
namespace SafeInit {

/**
* A TargetHandle functions as a weak reference to a Target. It is how an implementation of
* SafeInit::Manager would safely tell a target to `initialize` with no guarantees about the
* target's lifetime. Typical usage (outside of SafeInit::ManagerImpl) does not require touching
* TargetHandles at all.
*/
struct TargetHandle {
virtual ~TargetHandle() = default;

/**
* Tell the target to begin initialization, if it is still available.
* @param watcher A Watcher for the target to notify when it has initialized.
* @return true if the target received this call, false if the target was already destroyed.
*/
virtual bool initialize(const Watcher& watcher) const PURE;
};
using TargetHandlePtr = std::unique_ptr<TargetHandle>;

/**
* An initialization Target is an entity that can be registered with a Manager for initialization.
* It can only be invoked through a TargetHandle.
*/
struct Target {
virtual ~Target() = default;

/**
* @return a human-readable target name, for logging / debugging.
*/
virtual absl::string_view name() const PURE;

/**
* Create a new handle that can initialize this target.
* @param name a human readable handle name, for logging / debugging.
* @return a new handle that can initialize this target.
*/
virtual TargetHandlePtr createHandle(absl::string_view name) const PURE;
};

} // namespace SafeInit
} // namespace Envoy
53 changes: 53 additions & 0 deletions include/envoy/safe_init/watcher.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#pragma once

#include <memory>

#include "envoy/common/pure.h"

#include "absl/strings/string_view.h"

namespace Envoy {
namespace SafeInit {

/**
* A WatcherHandle functions as a weak reference to a Watcher. It is how an implementation of
* SafeInit::Target would safely notify a Manager that it has initialized, and likewise it's how
* an implementation of SafeInit::Manager would safely tell its client that all registered targets
* have initialized, with no guarantees about the lifetimes of the manager or client. Typical usage
* (outside of SafeInit::TargetImpl and ManagerImpl) does not require touching WatcherHandles at
* all.
*/
struct WatcherHandle {
virtual ~WatcherHandle() = default;

/**
* Tell the watcher that initialization has completed, if it is still available.
* @return true if the watcher received this call, false if the watcher was already destroyed.
*/
virtual bool ready() const PURE;
};
using WatcherHandlePtr = std::unique_ptr<WatcherHandle>;

/**
* A Watcher is an entity that listens for notifications that either an initialization target or
* all targets registered with a manager have initialized. It can only be invoked through a
* WatcherHandle.
*/
struct Watcher {
virtual ~Watcher() = default;

/**
* @return a human-readable target name, for logging / debugging.
*/
virtual absl::string_view name() const PURE;

/**
* Create a new handle that can notify this watcher.
* @param name a human readable handle name, for logging / debugging.
* @return a new handle that can notify this watcher.
*/
virtual WatcherHandlePtr createHandle(absl::string_view name) const PURE;
};

} // namespace SafeInit
} // namespace Envoy
40 changes: 40 additions & 0 deletions source/common/safe_init/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
licenses(["notice"]) # Apache 2

load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_library",
"envoy_package",
)

envoy_package()

envoy_cc_library(
name = "watcher_lib",
srcs = ["watcher_impl.cc"],
hdrs = ["watcher_impl.h"],
deps = [
"//include/envoy/safe_init:watcher_interface",
"//source/common/common:logger_lib",
],
)

envoy_cc_library(
name = "target_lib",
srcs = ["target_impl.cc"],
hdrs = ["target_impl.h"],
deps = [
"//include/envoy/safe_init:target_interface",
"//source/common/common:logger_lib",
],
)

envoy_cc_library(
name = "manager_lib",
srcs = ["manager_impl.cc"],
hdrs = ["manager_impl.h"],
deps = [
":watcher_lib",
"//include/envoy/safe_init:manager_interface",
"//source/common/common:logger_lib",
],
)
79 changes: 79 additions & 0 deletions source/common/safe_init/manager_impl.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#include "common/safe_init/manager_impl.h"

#include "common/common/assert.h"

namespace Envoy {
namespace SafeInit {

ManagerImpl::ManagerImpl(absl::string_view name)
: name_(fmt::format("init manager {}", name)), state_(State::Uninitialized), count_(0),
watcher_(name_, [this]() { onTargetReady(); }) {}

Manager::State ManagerImpl::state() const { return state_; }

void ManagerImpl::add(const Target& target) {
++count_;
TargetHandlePtr target_handle(target.createHandle(name_));
switch (state_) {
case State::Uninitialized:
// If the manager isn't initialized yet, save the target handle to be initialized later.
ENVOY_LOG(debug, "added {} to {}", target.name(), name_);
target_handles_.push_back(std::move(target_handle));
return;
case State::Initializing:
// If the manager is already initializing, initialize the new target immediately. Note that
// it's important in this case that count_ was incremented above before calling the target,
// because if the target calls the init manager back immediately, count_ will be decremented
// here (see the definition of watcher_ above).
target_handle->initialize(watcher_);
return;
case State::Initialized:
// If the manager has already completed initialization, consider this a programming error.
ASSERT(false, fmt::format("attempted to add {} to initialized {}", target.name(), name_));
}
}

void ManagerImpl::initialize(const Watcher& watcher) {
// If the manager is already initializing or initialized, consider this a programming error.
ASSERT(state_ == State::Uninitialized, fmt::format("attempted to initialize {} twice", name_));

// Create a handle to notify when initialization is complete.
watcher_handle_ = watcher.createHandle(name_);

if (count_ == 0) {
// If we have no targets, initialization trivially completes. This can happen, and is fine.
ENVOY_LOG(debug, "{} contains no targets", name_);
ready();
} else {
// If we have some targets, start initialization...
ENVOY_LOG(debug, "{} initializing", name_);
state_ = State::Initializing;

// Attempt to initialize each target. If a target is unavailable, treat it as though it
// completed immediately.
for (const auto& target_handle : target_handles_) {
if (!target_handle->initialize(watcher_)) {
onTargetReady();
}
}
}
}

void ManagerImpl::onTargetReady() {
// If there are no remaining targets and one mysteriously calls us back, this manager is haunted.
ASSERT(count_ != 0, fmt::format("{} called back by target after initialization complete"));

// If there are no uninitialized targets remaining when called back by a target, that means it was
// the last. Signal `ready` to the handle we saved in `initialize`.
if (--count_ == 0) {
ready();
}
}

void ManagerImpl::ready() {
state_ = State::Initialized;
watcher_handle_->ready();
}

} // namespace SafeInit
} // namespace Envoy
Loading