Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Comment thread
mattklein123 marked this conversation as resolved.

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.
Comment thread
mergeconflict marked this conversation as resolved.
* @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_, std::bind(&ManagerImpl::onTargetReady, this)) {}

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_);
Comment thread
mergeconflict marked this conversation as resolved.
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