Skip to content

Commit db32337

Browse files
committed
iox-1002: Async API design draft
Signed-off-by: Pawel Rutka <pawel.rutka.ext@qorix.ai>
1 parent 654e516 commit db32337

6 files changed

Lines changed: 380 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) 2023 Contributors to the Eclipse Foundation
2+
//
3+
// See the NOTICE file(s) distributed with this work for additional
4+
// information regarding copyright ownership.
5+
//
6+
// This program and the accompanying materials are made available under the
7+
// terms of the Apache Software License 2.0 which is available at
8+
// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license
9+
// which is available at https://opensource.org/licenses/MIT.
10+
//
11+
// SPDX-License-Identifier: Apache-2.0 OR MIT
12+
13+
pub struct Listener<Service>
14+
where
15+
Service: service::Service,
16+
{
17+
listener: iceoryx2::port::listener::Listener<Service>,
18+
io: BridgedFd<RawFdBridge<AsyncSelector>>,
19+
}
20+
21+
impl<Service> Listener<Service>
22+
where
23+
Service: service::Service,
24+
<Service::Event as iceoryx2_cal::event::Event>::Listener: FileDescriptorBased,
25+
{
26+
pub(crate) fn from(listener: iceoryx2::port::listener::Listener<Service>) -> Result<Self, CommonErrors> {
27+
// Safety:
28+
// - This FD is owned by iceoryx2 listener and we don't close it on drop of RawFdBridge
29+
// - The FD is kept along with listener so lifetime is take care of
30+
// - Each Listener has its own FD so no sharing is done in iceoryx2 layer
31+
let fd = unsafe { listener.file_descriptor().native_handle() };
32+
33+
Ok(Self {
34+
listener,
35+
io: BridgedFd::new_with_interest(RawFdBridge::from(fd)?, IoEventInterest::READABLE)?,
36+
})
37+
}
38+
39+
/// Returns the [`UniqueListenerId`] of the [`Listener`]
40+
pub fn id(&self) -> UniqueListenerId {
41+
self.listener.id()
42+
}
43+
44+
/// Returns the deadline of the corresponding [`Service`](crate::service::Service).
45+
pub fn deadline(&self) -> Option<Duration> {
46+
self.listener.deadline()
47+
}
48+
49+
/// Async wait for a new [`EventId`]. On error it returns [`ListenerWaitError`] is returned which describes
50+
/// the error in detail.
51+
pub async fn wait_one(&self) -> Result<EventId, ListenerWaitError> {
52+
self.io
53+
.async_call(IoEventInterest::READABLE, |raw_fd| {
54+
raw_fd.io_call(|fd| {
55+
info!("Checking for Iceoryx event on fd: {}", fd);
56+
self.wait_one_internal()
57+
})
58+
})
59+
.await
60+
.map_err(|_| ListenerWaitError::InternalFailure)
61+
.and_then(|r| match r {
62+
Ok(event) => Ok(event),
63+
Err(e) => Err(e),
64+
})
65+
}
66+
67+
fn wait_one_internal(&self) -> IoResult<Result<EventId, ListenerWaitError>> {
68+
loop {
69+
match self.listener.try_wait_one() {
70+
Ok(event) if event.is_some() => return Ok(Ok(event.unwrap())),
71+
Ok(_) => {
72+
// This is None, so there was and error, probably EAGAIN or EWOULDBLOCK
73+
if std::io::Error::last_os_error().kind() == std::io::ErrorKind::WouldBlock {
74+
error!("Iceoryx listener would block, should do re-register!... {}", unsafe {
75+
self.listener.file_descriptor().native_handle()
76+
});
77+
return Err(std::io::ErrorKind::WouldBlock.into());
78+
} else {
79+
panic!("Something went wrong!");
80+
}
81+
}
82+
Err(ListenerWaitError::InterruptSignal) => {
83+
continue;
84+
}
85+
Err(e) => {
86+
error!("Error waiting for Iceoryx event: {}", e);
87+
return Ok(Err(e));
88+
}
89+
}
90+
}
91+
}
92+
}
93+
94+
impl<T> Drop for Listener<T>
95+
where
96+
T: service::Service,
97+
{
98+
fn drop(&mut self) {
99+
// Leave the underlying fd open, as we don't own it and let iceoryx2 handle it
100+
self.io.close_on_drop(false);
101+
}
102+
}
103+
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# Async API Design Document
2+
3+
## Remarks
4+
5+
For this document currently we describe `Events` and `PubSub` however this
6+
analogy shall continue for other messaging patterns later on.
7+
8+
## Terminology
9+
10+
## Overview
11+
12+
A high-level pitch of the feature:
13+
14+
* The new `async` API will provide non_blocking, but still linear, behavior in
15+
the code thanks to `async/await`
16+
* The new `async` API will solve a usage of `iceoryx2` API in `async` code which
17+
currently need to be handmade manually either via:
18+
* polling and repetition logic within functions, causing additional not needed
19+
code on user side
20+
* custom WaitSet usage in separate thread and a bridge to `async` world.
21+
* or some other custom work
22+
* The `async` usage in Rust is already well established technique that is
23+
adopted by many crates, including those with highest usage
24+
25+
## Requirements
26+
27+
* **R1: Async API look and feel** \* The new `async` API shall provide the same
28+
look and feel as standard one
29+
30+
## Use Cases
31+
32+
### Use-Case 1: Waiting for an event
33+
34+
* **As a** developer
35+
* **I want** to wait on `event` API
36+
* **So that** it does not block current thread and continues only once event is
37+
delivered
38+
39+
### Use-Case 2: Waiting for a new sample
40+
41+
* **As a** developer
42+
* **I want** to wait on `new sample` in pub-sub
43+
* **So that** it does not block current thread and continues only once sample is
44+
delivered
45+
46+
## Usage
47+
48+
### Example: Await on events
49+
50+
```rust
51+
let node = NodeBuilder::new().create::<ipc_threadsafe::Service>().unwrap();
52+
53+
let event = node
54+
.service_builder(&"MyEventName".try_into().unwrap())
55+
.event()
56+
.open_or_create()
57+
.unwrap();
58+
59+
let listener = event.listener_builder().create_async().unwrap();
60+
61+
println!("Awaiting for Iceoryx event in batches while doing something else ...");
62+
63+
listener
64+
.wait_all(&mut |event_id| {
65+
print!("Received Iceoryx event: {:?}\n", event_id);
66+
})
67+
.await
68+
.unwrap();
69+
```
70+
71+
## Implementation
72+
73+
### Achieving async API
74+
75+
Since all iceoryx2 messaging patterns (except `Event`) are poll based, we need
76+
to pair them them with the `Event` to achieve possibility to react in `async`
77+
API only on change of data. This means:
78+
79+
* `PubSub` should be paired with `Event`
80+
* `RequestResponse` should be paired with `Event`
81+
* and so on
82+
83+
Due to this, further document assumes direct usage of high level `Event`
84+
messaging pattern to facilitate the feature.
85+
86+
> DISCLAIMER: IT may be that iceoryx2 authors will have better idea than that.
87+
> The only issue I do see now is probably impact on zero-trust deployment during
88+
> configuration where ie. async PubSub Producer shall also have rights to be
89+
> Event Notifier on some connected topic.
90+
91+
### Split of code
92+
93+
The main idea is to split source code into two parts:
94+
95+
1. The Event implementation that is both `OS` dependent and `runtime` dependent
96+
since it incurs some IO call
97+
2. All the other API that do need `Event` implementation, but the rest is
98+
purely `runtime` independent and can be pure `async`
99+
100+
To facilitate above, below class diagram is showing one of solution
101+
102+
![Class Diagram](new_classes.svg)
103+
104+
### Building objects
105+
106+
The next step is to provide a way to build objects that provide `async` API.
107+
108+
There are two approaches that can be chosen for implementation:
109+
110+
#### 1. Use custom event for each pair (messaging patern, data type) based on
111+
`ServiceName`
112+
113+
##### Pros
114+
115+
* all messaging pattern in service will work as usual
116+
* no limitations
117+
118+
##### Cons
119+
120+
* dynamic Service creation to obtain event for each messaging pattern, like for
121+
`ServiceNameABC` (PubSub, int) we need to create also internally service
122+
`ServiceNameABC/__internal_pubsub_event` to obtain event for notifications
123+
124+
#### 2. Use event from the service `ServiceName`
125+
126+
##### Pros
127+
128+
* no need to create dynamic event name
129+
130+
##### Cons
131+
132+
* limit a service to only single messaging pattern as using event will cause no
133+
way to use it again
134+
135+
Considering above, continuation is done based on option 1. Below shows only
136+
small snippet where extension for creating object can be placed.
137+
138+
![Port factory](port_factory.svg)
139+
140+
### Implementation
141+
142+
#### AsyncListener - `messaging-patter == event`
143+
144+
This is purely `runtime` specific implementation but is currently doable with
145+
`non-blocking` api of `Listener`. Working example:
146+
[Code sample](assets/event_example.rs) During implementation it may come
147+
beneficial to either expose some properties (of current listener) or `add` new
148+
sync api with different signature.
149+
150+
#### AsyncSubscriber - `messaging-patter == pubsub`
151+
152+
Pure `async` implementation not using any specifics of `runtime` shall be
153+
doable. In case some unexpected dependency will be needed it has to be exposed
154+
over defined abstraction same as `AsyncListenerTrait`
155+
156+
#### Builder
157+
158+
Implement the `Builders` extensions to they can provide `async` versions of objects
159+
160+
## Certification & Safety-Critical Usage
161+
162+
Answer:
163+
164+
* Applicable standards (e.g., ASIL-D, ISO 26262)
165+
* Support for **zero-trust deployments**: can rogue processes break it?
166+
* Evidence for claims (e.g., subscribers cannot corrupt publisher-owned
167+
read-only memory)
168+
* Real-time suitability: any blocking calls, background threads, or
169+
indeterminism?
170+
* If unsuitable for zero-trust or real-time use, how do we prevent accidental
171+
misuse?
172+
173+
PR: To me idea, we will build async API on top of existing non-blocking, non
174+
async implementation and the API will be just a thin wrapper. The connection to
175+
specific `runtime` is outside of project and in gesture of specific `runtime` to
176+
guarantee any of above.
177+
178+
## Milestones
179+
180+
### Milestone 1 – Provide Traits and object skeletons
181+
182+
* TBD later
183+
184+
**Results:**
185+
186+
* User will see that there is ongoing work on `async` API
187+
188+
### Milestone 2 – Implement Event in Async Runtime
189+
190+
* TBD later
191+
192+
**Results:**
193+
194+
* User will get first support for async API for specific runtime. This will also
195+
open a way for other to implement a bridge to other runtime like `tokio` as
196+
basic idea will be shown
197+
198+
### Milestone 2 – Implement PubSub
199+
200+
* TBD later
201+
202+
**Results:**
203+
204+
* PubSub API will have `async` API available
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package iceoryx2 {
2+
3+
interface AsyncListenerTrait<Trait> {
4+
' /// Create new instance of async Listener
5+
+ fn new(listener: iceoryx2::Listener) -> Self;
6+
' /// Returns the [`UniqueListenerId`] of the [`Listener`]
7+
+ fn id(&self) -> UniqueListenerId;
8+
' /// Returns the deadline of the corresponding [`Service`](crate::service::Service).
9+
+ fn deadline(&self) -> Option<Duration>;
10+
' /// Async wait for a new [`EventId`]. On error it returns [`AsyncListenerWaitError`] is returned which describes
11+
' /// the error in detail.
12+
+ fn wait_one(&self) -> impl Future<Output = Result<EventId, AsyncListenerWaitError>>;
13+
' /// Async wait for new [`EventId`]s. Collects all [`EventId`]s that were received and
14+
' /// calls the provided callback is with the [`EventId`] as input argument. This will `await` until callback is called at least once
15+
+ fn wait_all<F: FnMut(EventId)>(&self, callback: &mut F) -> impl Future<Output = Result<(), AsyncListenerWaitError>>;
16+
}
17+
18+
enum AsyncListenerWaitError {
19+
ContractViolation = ListenerWaitError::ContractViolation,
20+
InternalFailure = ListenerWaitError::InternalFailure,
21+
...
22+
}
23+
24+
25+
class AsyncSubscriber <<Service,Payload,UserHeader, EventListener: AsyncListenerTrait>> {
26+
sync_subscriber: iceoryx2::port::subscriber::Subscriber<Service, Payload, UserHeader>,
27+
listener: EventListener,
28+
29+
+ API follows as in usual Subscriber ie:
30+
+ async fn receive(&self) -> Result<Sample<Service, Payload, UserHeader>, AsyncReceiveError>
31+
32+
}
33+
34+
class AsyncConnector<Trait> {
35+
type ListenerType: AsyncListenerTrait;
36+
37+
}
38+
39+
note top of AsyncConnector
40+
Let us group external async properties in one place.
41+
This will come in handy if it happens
42+
that we need to know more unknown types that depend on runtime implementation.
43+
Good example can be `Sleep`.
44+
end note
45+
46+
}
47+
48+
package "Some Runtime / Runtime Adapter Crate" {
49+
50+
class AsyncListener {
51+
- listener: iceoryx2::Listener
52+
}
53+
54+
AsyncListener ..|> AsyncListenerTrait
55+
AsyncSubscriber --> AsyncListenerTrait
56+
}

0 commit comments

Comments
 (0)