-
Notifications
You must be signed in to change notification settings - Fork 1k
runtime: I/O API + simulator #5658
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
Open
kim
wants to merge
11
commits into
master
Choose a base branch
from
kim/sim-io
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
d7f1a6b
WIP: I/O API + simulator
kim 0f32031
Use SimulatorIO as the "I/O driver" in the executor
kim 98d7d23
Make it clearer that we're clearing
kim 16a1fc7
Expose ways for the runtime to inject failures.
kim 99638a6
Encapsulate SimulatorIO in an I/O "driver" that can inject failures.
kim b3284d8
Remove TODO
kim 87a9c19
Merge branch 'master' into kim/sim-io
kim e9378c3
Fix optional dependencies
kim f868c37
Fix windows
kim 031d709
Fix fix windows
kim 8f246ca
Merge branch 'master' into kim/sim-io
kim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; | ||
|
|
||
| /// Size in bytes of a disk sector. | ||
| pub const SECTOR_SIZE: usize = 4096; | ||
|
|
||
| /// Types that can be safely converted to and from sector-aligned byte slices. | ||
| pub trait AlignedBytes: Sized { | ||
| /// Assert that the type' size is a multiple of [SECTOR_SIZE] and has the | ||
| /// right aligment. | ||
| /// | ||
| /// NOTE: Associated constants are evaluated lazily -- add a free | ||
| /// | ||
| /// `const _: () = <T as AlignedBytes>::ASSERT_VALID_LAYOUT;` | ||
| /// | ||
| /// for each `T` that is supposed to be used as an `AlignedBytes`. | ||
| const ASSERT_VALID_LAYOUT: () = { | ||
| assert!(align_of::<Self>() == SECTOR_SIZE); | ||
| assert!(size_of::<Self>().is_multiple_of(SECTOR_SIZE)); | ||
| }; | ||
|
|
||
| /// Reinterpret `self` as a byte slice. | ||
| /// | ||
| /// The returned slice will be of length `size_of::<Self>()`. | ||
| fn as_bytes(&self) -> &[u8]; | ||
|
|
||
| /// Reinterpret `self` as a mutable byte slice. | ||
| /// | ||
| /// The returned slice will be of length `size_of::<Self>()`. | ||
| fn as_bytes_mut(&mut self) -> &mut [u8]; | ||
|
|
||
| /// Reinterpret a byte slice as `Self`. | ||
| /// | ||
| /// The slice must be of length `size_of::<Self>()`. | ||
| /// | ||
| /// NOTE: Any slice of the right size, but consisting of only `0` (zero) | ||
| /// bytes can be converted to `Self`. It is the caller's responsibility to | ||
| /// validate the returned type as per the application's invariants. | ||
| /// | ||
| /// # Panics | ||
| /// | ||
| /// Panics if `b.len() != size_of::<Self>()`. | ||
| fn from_bytes(b: &[u8]) -> Self; | ||
| } | ||
|
|
||
| impl<T: FromBytes + IntoBytes + KnownLayout + Immutable> AlignedBytes for T { | ||
| fn as_bytes(&self) -> &[u8] { | ||
| <T as IntoBytes>::as_bytes(self) | ||
| } | ||
|
|
||
| fn as_bytes_mut(&mut self) -> &mut [u8] { | ||
| <T as IntoBytes>::as_mut_bytes(self) | ||
| } | ||
|
|
||
| fn from_bytes(b: &[u8]) -> Self { | ||
| Self::read_from_bytes(b).unwrap() | ||
| } | ||
| } | ||
|
|
||
| /// An error `E`, along with auxiliary data `T`. | ||
| /// | ||
| /// `T` is usually a buffer of type [AlignedBytes], whose ownership is | ||
| /// transferred back to the caller when an error occurs. | ||
| /// | ||
| /// As this type signifies an error condition, the contents of `T` are | ||
| /// unspecified. | ||
| #[derive(Debug)] | ||
| pub struct ErrorWith<E, T> { | ||
| pub error: E, | ||
| pub with: T, | ||
| } | ||
|
|
||
| /// The canonical, low-level I/O API. | ||
| /// | ||
| /// Currently only supports file I/O, but eventually all I/O performed by | ||
| /// SpacetimeDB should go through this trait. | ||
| /// | ||
| /// Intended to support implementations based on `io-uring`, which means that | ||
| /// buffer ownership is transferred to the I/O engine while reading or writing. | ||
| /// | ||
| /// Implementations should be `!Send`, i.e. all I/O happens on a single thread. | ||
| /// | ||
| /// File operations should never be mutually exclusive, and therefore expose a | ||
| /// `pwrite`/`pread`-style API. It is assumed that direct I/O (`O_DIRECT`) is | ||
| /// used, i.e. the kernel page cache is bypassed. The [AlignedBytes] type | ||
| /// ensures that the alignment requirements for direct I/O are met. | ||
| pub trait SpacetimeIO { | ||
| /// An open file handle. | ||
| /// | ||
| /// Like [std::fs::File], the file shall be closed when the last reference | ||
| /// to the handle is dropped. | ||
| /// | ||
| /// Unlike [std::fs::File], the file handle must be clone-able. | ||
| type Fd: Clone; | ||
| /// The error returned by methods of this trait. | ||
| /// | ||
| /// This should always be instantiated to [std::io::Error]. However, pending | ||
| /// [alloc_io], this type is not in `core`, which would prevent this crate | ||
| /// from being `no_std`. | ||
| /// | ||
| /// [alloc_io]: https://github.com/rust-lang/rust/issues/154046 | ||
| type Error; | ||
|
|
||
| /// Open the file at `path`. | ||
| fn open_file(&self, path: &str) -> impl Future<Output = Result<Self::Fd, Self::Error>>; | ||
|
|
||
| /// Create the file at `path` and allocate `len` bytes. | ||
| /// | ||
| /// Returns an error if the file already exists. | ||
| fn create_file(&self, path: &str, len: u64) -> impl Future<Output = Result<Self::Fd, Self::Error>>; | ||
|
|
||
| /// Write `buf` to `fd` at `offset`. | ||
| /// | ||
| /// `offset` must be a multiple of [SECTOR_SIZE]. | ||
| /// | ||
| /// Behaves like `FileExt::write_all_at`, i.e. tries to write all bytes in | ||
| /// `buf`, potentially retrying on errors of kind interrupted, and returns | ||
| /// an error if that fails. | ||
| fn write_all_at<B: AlignedBytes + Send + 'static>( | ||
| &self, | ||
| fd: Self::Fd, | ||
| buf: B, | ||
| offset: u64, | ||
| ) -> impl Future<Output = Result<B, ErrorWith<Self::Error, B>>>; | ||
|
|
||
| /// Read `size_of::<B>()` bytes from `fd` at `offset` and interpret them at | ||
| /// type `B`. | ||
| /// | ||
| /// `offset` must be a multiple of [SECTOR_SIZE]. | ||
| /// | ||
| /// Behaves like `FileExt::read_exact_at`, i.e. attempts to read | ||
| /// `size_of::<B>()` bytes, potentially retrying on errors of kind | ||
| /// interrupted, and returns an error if less than the required bytes could | ||
| /// be read. | ||
| fn read_exact_at<B: AlignedBytes + Send + 'static>( | ||
| &self, | ||
| fd: Self::Fd, | ||
| buf: B, | ||
| offset: u64, | ||
| ) -> impl Future<Output = Result<B, ErrorWith<Self::Error, B>>>; | ||
|
|
||
| /// Call `fsync(2)` on `fd`. | ||
| fn fsync(&self, fd: Self::Fd) -> impl Future<Output = Result<(), Self::Error>>; | ||
| /// Call `fdatasync(2)` on `fd`. | ||
| fn fdatasync(&self, fd: Self::Fd) -> impl Future<Output = Result<(), Self::Error>>; | ||
|
|
||
| /// Allocate `additional` bytes for the file `fd`. | ||
| fn reserve(&self, fd: Self::Fd, additional: u64) -> impl Future<Output = Result<(), Self::Error>>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,3 +7,5 @@ extern crate std; | |
|
|
||
| #[cfg(feature = "sim")] | ||
| pub mod sim; | ||
|
|
||
| pub mod io; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| use crate::sim::{io::SimulatorIO, Rng}; | ||
|
|
||
| #[derive(Clone, Copy, Debug, PartialEq)] | ||
| pub struct Config { | ||
| /// The max number of submissions to run per [Driver::tick]. | ||
| pub max_submissions_per_tick: usize, | ||
| /// The max number of completions to finish per [Driver::tick]. | ||
| pub max_completions_per_tick: usize, | ||
| /// Submission reordering probability. | ||
| /// | ||
| /// Describes the probability by which to select the next submission queue | ||
| /// entry randomly, as opposed to the oldest entry in the queue. | ||
| pub prob_reorder_submissions: f64, | ||
| /// Completion reordering probability. | ||
| /// | ||
| /// Describes the probability by which to select the next completion queue | ||
| /// entry randomly, as opposed to the oldest entry in the queue. | ||
| pub prob_reorder_completions: f64, | ||
| /// Probability by which to skip one submission queue entry. | ||
| /// | ||
| /// If skipped, the entry still counts towards `max_submissions_per_tick`. | ||
| pub prob_skip: f64, | ||
| /// Probability by which to cancel a submission queue entry. | ||
| /// | ||
| /// [crate::sim::io::op::Submission::cancel()] is called on the entry, which | ||
| /// may generate a completion. | ||
| pub prob_cancel: f64, | ||
| } | ||
|
|
||
| impl Default for Config { | ||
| fn default() -> Self { | ||
| Self { | ||
| max_submissions_per_tick: 1, | ||
| max_completions_per_tick: 1, | ||
| prob_reorder_submissions: 0.0, | ||
| prob_reorder_completions: 0.0, | ||
| prob_skip: 0.0, | ||
| prob_cancel: 0.0, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub struct Driver { | ||
| io: SimulatorIO, | ||
| config: Config, | ||
| } | ||
|
|
||
| impl Driver { | ||
| pub fn new(config: Config) -> Self { | ||
| Self { | ||
| io: <_>::default(), | ||
| config, | ||
| } | ||
| } | ||
|
|
||
| /// Advance the I/O simulator according the [Config]. | ||
| /// | ||
| /// Returns `true` if progress has been made, or there are pending entries | ||
| /// in either the submission or completion queue. | ||
| pub fn tick(&self, rng: &Rng) -> bool { | ||
| let mut progress = false; | ||
| for _ in 0..self.config.max_submissions_per_tick { | ||
| if !rng.buggify_with_prob(self.config.prob_skip) { | ||
| let sqe = if rng.buggify_with_prob(self.config.prob_reorder_submissions) { | ||
| self.io.random_submission(rng) | ||
| } else { | ||
| self.io.next_submission() | ||
| }; | ||
|
|
||
| if let Some(sqe) = sqe { | ||
| if rng.buggify_with_prob(self.config.prob_cancel) { | ||
| sqe.cancel(); | ||
| } else { | ||
| self.io.execute(sqe); | ||
| } | ||
| progress = true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for _ in 0..self.config.max_completions_per_tick { | ||
| let cqe = if rng.buggify_with_prob(self.config.prob_reorder_completions) { | ||
| self.io.random_completion(rng) | ||
| } else { | ||
| self.io.next_completion() | ||
| }; | ||
|
|
||
| if let Some(cqe) = cqe { | ||
| cqe.complete(); | ||
| progress = true | ||
| } | ||
| } | ||
|
|
||
| progress |= self.io.pending(); | ||
| progress | ||
| } | ||
|
|
||
| pub fn io(&self) -> &SimulatorIO { | ||
| &self.io | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my network side implementation, I have taken a different approach (inspired from tigerbeetle), added duration_at field to events/ops. Which has Driver induced randomness injected from config.
I think for this will translate here like, individual Disk IO ops to have
duration_atfield, which needs to be expired for events to go in completion queue.I am wondering if we do
duration_atfor Disk IO (which we should) then whether currentio::Configwould be any useful? It doesn't look like both approaches compliments each other.Also, I have made my
Networktick just towaker.wake()all expired tasks every time, as randomness is induced byduration_atitself and furtherrun_all_readyalso picks the tasks randomly to poll.Wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to keep the distinction between SQE executing (and effects potentially visible) and completion future resolving -- I expect this gap to be a potential source of bugs.
Other than that, we can do scheduling based on time if you prefer. We can also just model io-uring's linked timer, so that we bake in cancellation first class.
I guess there should also be an I/O queue per node?