Skip to content
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

Iox 1640 create polymorphic singleton abstraction #1656

Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2bf8488
iox-#1640 Polymorphic handler implementation
MatthiasKillat Sep 19, 2022
aef2103
iox-#1640 Polymorphic handler test
MatthiasKillat Sep 19, 2022
d88805e
iox-#1640 Test Acivatable concept
MatthiasKillat Sep 20, 2022
2a6e336
iox-#1640 Document PolymorphicHandler
MatthiasKillat Sep 20, 2022
f38f187
iox-#1640 Move PolymorphicHandler impl
MatthiasKillat Sep 20, 2022
235739f
iox-#1640 Update PolymorphicHandler tests
MatthiasKillat Sep 20, 2022
f2baec6
iox-#1640 Add hooks to PolymorphicHandler
MatthiasKillat Sep 20, 2022
c07c569
iox-#1640 Move PolymorphicSingleton impl
MatthiasKillat Sep 21, 2022
ddbf37c
iox-#1640 Add StaticLifetimeGuard
MatthiasKillat Oct 19, 2022
b97eeaf
iox-#1640 Remove mutex
MatthiasKillat Oct 21, 2022
4692854
iox-#1640 Move StaticLifetimeGuard impl
MatthiasKillat Oct 24, 2022
90c756f
iox-#1640 PolymorphicHandler set by guard
MatthiasKillat Oct 24, 2022
0644af9
iox-#1640 Make StaticLifetimeGuard assignable
MatthiasKillat Oct 24, 2022
5a6be27
iox-#1640 Add design document
MatthiasKillat Oct 26, 2022
7722618
iox-#1640 Update documentation
MatthiasKillat Nov 9, 2022
78f9443
iox-#1640 Optimize memory order
MatthiasKillat Nov 15, 2022
cf6dc33
iox-#1640 Correct activation during self exchange
MatthiasKillat Nov 15, 2022
65e2ec6
iox-#1640 Remove Activatable restriction
MatthiasKillat Nov 30, 2022
a26d747
iox-#1640 Correct finalize and reset test
MatthiasKillat Nov 30, 2022
d67f4dc
iox-#1640 Update documentation
MatthiasKillat Dec 1, 2022
b2d2029
iox-#1640 Make setCount protected
MatthiasKillat Jan 18, 2023
0abe2a6
iox-#1640 Weakened counter modification
MatthiasKillat Jan 18, 2023
f5c444b
iox-#1640 Make StaticLifetimeGuard tests independent
MatthiasKillat Jan 18, 2023
614c94a
iox-#1640 Optimize PolymorphicHandler
MatthiasKillat Jan 19, 2023
00486cf
iox-#1640 Let set and reset return bool
MatthiasKillat Jan 19, 2023
3bf204b
iox-#1640 Update iceoryx-unreleased.md
MatthiasKillat Jan 19, 2023
97793c8
iox-#1640 Add concurrent guard test
MatthiasKillat Jan 24, 2023
193b4c6
iox-#1640 Update tests
MatthiasKillat Jan 30, 2023
b35cb02
iox-#1640 Correct concurrent StaticLifetimeGuard destruction
MatthiasKillat Jan 30, 2023
4c259b9
iox-#1640 Update comments and documentation
MatthiasKillat Jan 31, 2023
fcf639d
iox-#1640 Let DefaultHooks call abort
MatthiasKillat Jan 31, 2023
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
8 changes: 8 additions & 0 deletions .clang-tidy-diff-scans.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@
./iceoryx_hoofs/source/posix_wrapper/shared_memory_object/*
./iceoryx_hoofs/test/moduletests/test_posix*
./iceoryx_hoofs/include/iceoryx_hoofs/design_pattern/builder.hpp
./iceoryx_hoofs/include/iceoryx_hoofs/design_pattern/polymorphic_handler.hpp
./iceoryx_hoofs/include/iceoryx_hoofs/design_pattern/static_lifetime_guard.hpp

./iceoryx_hoofs/include/iceoryx_hoofs/internal/design_pattern/polymorphic_handler.inl
./iceoryx_hoofs/include/iceoryx_hoofs/internal/design_pattern/static_lifetime_guard.inl

./iceoryx_hoofs/test/moduletests/test_polymorphic_handler.cpp
./iceoryx_hoofs/test/moduletests/test_static_lifetime_guard.cpp

./iceoryx_hoofs/container/include/iox/**/*
./iceoryx_hoofs/test/moduletests/test_container_*
Expand Down
243 changes: 243 additions & 0 deletions doc/design/polymorphic_handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# Polymorphic Handler

## Goals

1. Manage a singleton handler (the current handler) that inherits from some interface `I`
1. Initialize the handler with some default instance of a class that inherits from `I`
1. Replace the singleton handler at runtime with a singleton of a different class
that also inherits from `I`
1. Ensure that there is some handler to access at all times (until program termination)
1. Ensure that any accessed singleton handler lives long enough to be accessed by other static
variables
1. Obtaining the current handler must be thread-safe
1. It must be possible to finalize the handler, i.e. prohibit any changes after it is finalized.
1. Any attempt to change the handler after it is finalized, shall call an function that has access
FerdinandSpitzschnueffler marked this conversation as resolved.
Show resolved Hide resolved
to the current and new handler (for e.g. logging).

To achieve this, we define another support class `StaticLifeTimeGuard` that solves the singleton lifetime problem.
FerdinandSpitzschnueffler marked this conversation as resolved.
Show resolved Hide resolved
This class can be used on its own and is based on the nifty counter reference counting.

While obtaining the instance is thread-safe, the instance managed by the handler may not be
thread-safe. If thread-safety of the instances is desired, the classes implementing `I`
must be thread-safe.

## StaticLifetimeGuard

### Properties

1. Manage a singleton instance of some type `T`
1. Lazy initialization on first use
1. Thread-safe instance construction
1. Provide a thread-safe way of obtaining the instance
1. An instance shall only be destroyed after the last existing `StaticLifetimeGuard` object is
destroyed (regardless of where the `StaticLifetimeGuard` is constructed).

In the following a `StaticLifetimeGuard` is also called guard for brevity.

## Using the StaticLifetimeGuard

### Guard some static singleton instance

```cpp
struct Foo {

};

// create a guard for Foo, note that the instance does not exist yet
static StaticLifetimeGuard<Foo> guard;

// get the instance and store a reference
static Foo& fooInstance = StaticLifetimeGuard<Foo>::instance();
elBoberido marked this conversation as resolved.
Show resolved Hide resolved

// the fooInstance is guaranteed destroyed after guard is destroyed
// guard could also be held by another static

// alternatively call a static function on the guard (well-defined)
static Foo& sameFooInstance = guard.instance();

// &fooInstance and &sameFooInstance are equal
```

### Manage static singleton lifetime dependencies

```cpp
struct Foo {

};

// Bar uses the fooInstance that is guaranteed to be destroyed
// after guard is destroyed in ~Bar
struct Bar {
StaticLifetimeGuard<Foo> guard;

void f() {
auto& instance = StaticLifetimeGuard<Foo>::instance();
// use instance
}
};

// The Foo singleton instance will outlive the Bar instance
static Bar& barInstance = StaticLifetimeGuard<Bar>::instance();
elBoberido marked this conversation as resolved.
Show resolved Hide resolved

```

This allows creating dependency graphs of static singleton objects. Any static singleton object that
requires another static singleton object simply has to keep a guard of the other singleton object as
a member. The restriction is that it **works with singletons only**, i.e. there can be only one
instance per class that is tracked like this. Hence it is not possible to ensure the lifetime of two
different `Foo` instances.

## StaticLifetimeGuard - implementation considerations

### Instance construction
- thread-safe using atomics only
- during concurrent initialization the initializing call is determined using compare and swap (CAS)
- concurrent access of the instance is delayed until the initializing call completes instance
initialization
- instance can be accessed like a regular singleton

The construction also creates a `StaticLifetimeGuard` with static lifetime, the primary guard. This ensures basic
static lifetime of the constructed instance (as if it would have been a static variable in a Meyers
singleton itself).

### Reference counting

- global static atomic reference counter (initially zero)
- construction (including copy/move) of each guard object increases the counter by one
- destruction decreases the counter by one
- if the counter reaches zero (last guard destroyed), the instance is destroyed **IF it was
constructed before**

### Instance destruction

The instance is only constructed when it is needed (i.e. lazily). There can be several
guard objects without an instance ever being constructed. In this case no instance destruction takes
place once the counter reaches zero.

Due to atomic counter decrement, the instance is destroyed exactly once.

### Instance construction after destruction

It is not possible to replace the instance once constructed due to the static primary guard.
Technically it would be possible by some static destructor after the primary guard (and all other
guards) are destroyed, but this happens only during program termination.

## Using the Polymorphic Handler

### Basic Usage

```cpp
struct Interface {
public:
virtual void foo() = 0;
};

struct DefaultHandler : public Interface {
void foo() override { /* ... */ }
};

struct OtherHandler : public Interface {
void foo() override { /* ... */ }
};

using Handler = PolymorphicHandler<Interface, DefaultHandler>;
```

The first time we access the handler, it is initialized as a DefaultHandler that is guarded
internally with a `StaticLifeTimeGuard`.

```cpp
// thread 1
// get a reference to the current handler
auto& handler = Handler::get();

// use the handler polymorphically
handler.foo();
```

A concurrent thread 2 may switch the handler to some other handler. This will not interfere with the
execution of `foo` in thread 1, which is still using the old handler (which is ensured to be alive
by a guard).

The default handler is part of the `PolymorphicHandler`, but any other handler to be set is not. To
ensure the lifetime of handlers that are used, the API to set another handler requires using a
guard. The guard is passed by value and an internal copy ensures any handler being set is not
destroyed before main exits. Any further lifetime must be controlled with external guards.

```cpp
// thread 2
StaticLifetimeGuard<OtherHandler> guard;

// the OtherHandler instance may not be constructed yet

// set the handler to the instance guarded by guard,
// this will create another guard to ensure the lifetime
Handler::set(guard);
FerdinandSpitzschnueffler marked this conversation as resolved.
Show resolved Hide resolved

// OtherHandler instance exists now,
// any other thread will eventually use the new handler

auto& handler = Handler::get();

// unless it was concurrently set again,
// this thread is guaranteed to use the OtherHandler instance
handler.foo();
```

### Lifetime of instances

Any thread using a handler (via `get`) will eventually use the handler that was set last (the order
is determined by atomic set operations).

Holding external references to handlers that were set once remain valid until main exits.
They are not updated to the latest handler on their own, `get` must be used to retrieve the latest
handler.

If externally set handlers are required to live even longer, explicit guards of them must be kept by
other static objects.

### Switching between multiple handlers

When a new handler is set by

```cpp
StaticLifetimeGuard<OtherHandler> guard;
Handler::set(guard);
```
(and the handler is not finalized) the following steps happen

1. Create a guard for the new handler
1. Obtain the new handler instance from the guard
1. Exchange the old handler with the new handler

Afterwards both handlers still exist (they have static lifetime), and using either works if a
reference to it is known. Any threads calling `Handler::get` will use the new handler, but there may
be threads that still use the old handler (as they are currently accessing it and have not called
`get` again).

Any thread keeps track of its latest known local handler using a `thread_local` variable. This local
handler is initialized the first time the thread uses `Handler`. This is guaranteed to provide the
latest handler instance, but the latest instance can change in the meantime.

The thread can then check whether the local handler is still considered by comparing it to the
global handler (which can be loaded with a relaxed atomic). If both are equal then it is considered
unchanged and the thread will proceed to use the local handler.
Otherwise it will obtain the new handler with a stronger memory synchronization (more costly).

Note that the current handler can change any time but there is no problem as all handlers remain
usbale during the entire program lifetime. Due to this, there are no issues like the ABA problem,
the worst thing that can happen is working with a outdated handler.
MatthiasKillat marked this conversation as resolved.
Show resolved Hide resolved

This does not require blocking and only relies on fairly cheap atomic operations.
Without using a mutex while using the handler, it is impossible that
threads will always use the latest handler (as it may change at any time).
However, this is not required, it only is required that a handler that is
obtained can be safely accessed. The latter is ensured by using `StaticLifetimeGuard`.

### Thread safety

While obtaining the handler is thread safe and any handler obtained has a guaranteed lifetime until
main exits (or longer, with explicit guards), the individual handlers are not necessarily
thread-safe. This has to be ensured by the implementation of the `Interface`. In particular
`DefaultHandler` and any other handler derived from `Interface` must be thread-safe if it is to be
used by multiple threads.
1 change: 1 addition & 0 deletions doc/website/release-notes/iceoryx-unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- Implement UninitializedArray [\#1614](https://github.com/eclipse-iceoryx/iceoryx/issues/1614)
- Implement BumpAllocator [\#1732](https://github.com/eclipse-iceoryx/iceoryx/issues/1732)
- Expand cmake configuration options to enable reducing shared memory consumption. [\#1803](https://github.com/eclipse-iceoryx/iceoryx/issues/1803)
- Implement PolymorphicHandler [\#1640](https://github.com/eclipse-iceoryx/iceoryx/issues/1640)

**Bugfixes:**

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) 2023 by Apex.AI Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

#ifndef IOX_HOOFS_DESIGN_PATTERN_POLYMORPHIC_HANDLER_HPP
#define IOX_HOOFS_DESIGN_PATTERN_POLYMORPHIC_HANDLER_HPP

#include <atomic>
#include <iostream>
FerdinandSpitzschnueffler marked this conversation as resolved.
Show resolved Hide resolved
#include <type_traits>

#include "iceoryx_hoofs/design_pattern/static_lifetime_guard.hpp"

namespace iox
{
namespace design_pattern
{
namespace detail
{

/// @brief default hooks for the PolymorphicHandler
/// @tparam Interface the handler interface
/// @note template hooks to avoid forced virtual inheritance
template <typename Interface>
struct DefaultHooks
{
/// @brief called if the polymorphic handler is set or reset after finalize
/// @param currentInstance the current instance of the handler singleton
/// @param newInstance the instance of the handler singleton to be set
static void onSetAfterFinalize(Interface& currentInstance, Interface& newInstance) noexcept;
FerdinandSpitzschnueffler marked this conversation as resolved.
Show resolved Hide resolved
};

} // namespace detail

/// @brief Implements a singleton handler that has a default instance and can be changed
/// to another instance at runtime. All instances have to derive from the same interface.
/// The singleton handler owns the default instance but all other instances are created externally.
/// @tparam Interface The interface of the handler instances.
/// @tparam Default The type of the default instance. Must be equal to or derive from Interface.
FerdinandSpitzschnueffler marked this conversation as resolved.
Show resolved Hide resolved
/// @tparam Hooks A struct that implements onSetAfterFinalize which is called when
/// attempting to set or reset the handler after finalize was called.
///
/// @note In the special case where Default equals Interface, no polymorphism is required.
/// It is then possible to e.g. switch between multiple instances of Default type.
MatthiasKillat marked this conversation as resolved.
Show resolved Hide resolved
/// @note The lifetime of external non-default instances must exceed the lifetime of the PolymorphicHandler.
/// @note The PolymorphicHandler is guaranteed to provide a valid handler during the whole program lifetime (static).
/// It is hence not advisable to have other static variables depend on the PolymorphicHandler.
/// It must be ensured that they are destroyed before the PolymorphicHandler.
/// @note Hooks must implement
/// static void onSetAfterFinalize(Interface& /*currentInstance*/, Interface& /*newInstance*/).
template <typename Interface, typename Default, typename Hooks = detail::DefaultHooks<Interface>>
class PolymorphicHandler
{
static_assert(std::is_base_of<Interface, Default>::value, "Default must inherit from Interface");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the documentation this is not fully true. They can be the same. Do you think that Interface must be the base of Default reflects this better?

Copy link
Contributor Author

@MatthiasKillat MatthiasKillat Jan 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a class is always the base of itself but have to check. At some point I tried to use it with itself and it worked so this is true.

In other words, the base_of relation should be reflexive, at least it would be reasonable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A class is always base of itself (as intended), so PolymorphicHandler<Default, Default> works. The consequence is that it is not really polymorphic anymore, but you still can switch between different Default instances. That is a niche feature but not detrimental and potentially useful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You misunderstood me. My point was that Default can be seen as the base of itself but I wouldn't say the Default inherited from itself. The message implies that this is the case, therefore my proposal to change the wording and use base instead of inherit.


using Self = PolymorphicHandler<Interface, Default, Hooks>;
friend class StaticLifetimeGuard<Self>;

public:
/// @brief obtain the current singleton instance
/// @return the current instance
/// @note we cannot be sure to use the current handler unless we call get,
/// i.e. a reference obtained from get may reference a previous handler
/// (that is still functional)
static Interface& get() noexcept;

/// @brief set the current singleton instance
/// @param handlerGuard a guard to the handler instance to be set
/// @return true if handler was set, false otherwise
/// @note the handler cannot be replaced if it was finalized
/// @note using a guard in the interface prevents the handler to be destroyed while it is used,
/// passing the guard by value is necessary (it has no state anyway)
/// @note If finalization takes effect here, setHandler will still change the handler
/// This is still correct concurrent behavior in the sense that it maps
/// to a sequential execution where the handler is set before finalization.
template <typename Handler>
static bool set(StaticLifetimeGuard<Handler> handlerGuard) noexcept;

/// @brief reset the current singleton instance to the default instance
/// @return true if handler was reset to default, false otherwise
/// @note the handler cannot be reset if it was finalized
static bool reset() noexcept;

/// @brief finalizes the instance, afterwards Hooks::onSetAfterFinalize
/// will be called during the remaining program lifetime
/// when attempting to set or reset the handler
static void finalize() noexcept;

/// @brief returns a lifetime guard whose existence guarantees
/// the created PolymorphicHandler singleton instance will exist at least as long as the guard.
/// @return opaque lifetime guard object for the (implicit) PolymorphicHandler instance
/// @note the PolymorphicHandler will exist once any of the static methods (get, set etc.)
/// are called
static StaticLifetimeGuard<Self> guard() noexcept;

private:
PolymorphicHandler() noexcept;

static PolymorphicHandler& self() noexcept;

static Default& getDefault() noexcept;

static Interface* getCurrentRelaxed() noexcept;

static Interface* getCurrentSync() noexcept;

static bool setHandler(Interface& handler) noexcept;

// should a defaultHandler be created, the guard prevents its destruction
StaticLifetimeGuard<Default> m_defaultGuard;
std::atomic_bool m_isFinal{false};
std::atomic<Interface*> m_current{nullptr};
};

} // namespace design_pattern
} // namespace iox

#include "iceoryx_hoofs/internal/design_pattern/polymorphic_handler.inl"

#endif // IOX_HOOFS_DESIGN_PATTERN_POLYMORPHIC_HANDLER_HPP
Loading