Skip to content

Commit ce027cb

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

6 files changed

Lines changed: 379 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: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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 `ServiceName`
111+
112+
##### Pros
113+
114+
* all messaging pattern in service will work as usual
115+
* no limitations
116+
117+
##### Cons
118+
119+
* dynamic Service creation to obtain event for each messaging pattern, like for
120+
`ServiceNameABC` (PubSub, int) we need to create also internally service
121+
`ServiceNameABC/__internal_pubsub_event` to obtain event for notifications
122+
123+
#### 2. Use event from the service `ServiceName`
124+
125+
##### Pros
126+
127+
* no need to create dynamic event name
128+
129+
##### Cons
130+
131+
* limit a service to only single messaging pattern as using event will cause no
132+
way to use it again
133+
134+
Considering above, continuation is done based on option 1. Below shows only
135+
small snippet where extension for creating object can be placed.
136+
137+
![Port factory](port_factory.svg)
138+
139+
### Implementation
140+
141+
#### AsyncListener - `messaging-patter == event`
142+
143+
This is purely `runtime` specific implementation but is currently doable with
144+
`non-blocking` api of `Listener`. Working example:
145+
[Code sample](assets/event_example.rs) During implementation it may come
146+
beneficial to either expose some properties (of current listener) or `add` new
147+
sync api with different signature.
148+
149+
#### AsyncSubscriber - `messaging-patter == pubsub`
150+
151+
Pure `async` implementation not using any specifics of `runtime` shall be
152+
doable. In case some unexpected dependency will be needed it has to be exposed
153+
over defined abstraction same as `AsyncListenerTrait`
154+
155+
#### Builder
156+
157+
Implement the `Builders` extensions to they can provide `async` versions of objects
158+
159+
## Certification & Safety-Critical Usage
160+
161+
Answer:
162+
163+
* Applicable standards (e.g., ASIL-D, ISO 26262)
164+
* Support for **zero-trust deployments**: can rogue processes break it?
165+
* Evidence for claims (e.g., subscribers cannot corrupt publisher-owned
166+
read-only memory)
167+
* Real-time suitability: any blocking calls, background threads, or
168+
indeterminism?
169+
* If unsuitable for zero-trust or real-time use, how do we prevent accidental
170+
misuse?
171+
172+
PR: To me idea, we will build async API on top of existing non-blocking, non
173+
async implementation and the API will be just a thin wrapper. The connection to
174+
specific `runtime` is outside of project and in gesture of specific `runtime` to
175+
guarantee any of above.
176+
177+
## Milestones
178+
179+
### Milestone 1 – Provide Traits and object skeletons
180+
181+
* TBD later
182+
183+
**Results:**
184+
185+
* User will see that there is ongoing work on `async` API
186+
187+
### Milestone 2 – Implement Event in Async Runtime
188+
189+
* TBD later
190+
191+
**Results:**
192+
193+
* User will get first support for async API for specific runtime. This will also
194+
open a way for other to implement a bridge to other runtime like `tokio` as
195+
basic idea will be shown
196+
197+
### Milestone 2 – Implement PubSub
198+
199+
* TBD later
200+
201+
**Results:**
202+
203+
* 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)