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 30 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
251 changes: 251 additions & 0 deletions doc/design/polymorphic_handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# 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 a function that has access
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.
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
bool success = Handler::set(guard);
if(!success)
{
// set may only fail after finalize
//
// do something in this case
// ...
// even if set was not successful, get() will return the previous handler
}

// 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 an outdated handler.

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
Loading