docsrs
only.Expand description
The internal implementation details of Access
.
Access
is the core trait of OpenDAL’s raw API. We operate
underlying storage services via APIs provided by Access
.
§Introduction
Access
can be split in the following parts:
// Attributes
#[async_trait]
// <----------Trait Bound-------------->
pub trait Accessor: Send + Sync + Debug + Unpin + 'static {
type Reader: oio::Read; // --+
type BlockingReader: oio::BlockingRead; // +--> Associated Type
type Lister: oio::Lister; // +
type BlockingLister: oio::BlockingLister; // --+
// APIs
async fn hello(&self, path: &str, args: OpCreate) -> Result<RpCreate>;
async fn world(&self, path: &str, args: OpCreate) -> Result<RpCreate>;
}
Let’s go deep into Access
line by line.
§Async Trait
At the first line of Access
, we will read:
#[async_trait]
This is an attribute from async_trait
. By using this attribute, we can write the following code without use nightly feature.
pub trait Accessor {
async fn create_dir(&self, path: &str) -> Result<()>;
}
async_trait
will transform the async fn
into:
pub trait Accessor {
fn create_dir<'async>(
&'async self,
) -> Pin<Box<dyn core::future::Future<Output = Result()> + MaybeSend + 'async>>
where Self: Sync + 'async;
}
It’s not zero cost, and we will improve this part once the related features are stabilised.
§Trait Bound
Then we will read the declare of Access
trait:
pub trait Accessor: Send + Sync + Debug + Unpin + 'static {}
There are many trait boundings here. For now, Access
requires the following bound:
Send
: Allow user to send between threads without extra wrapper.Sync
: Allow user to sync between threads without extra lock.Debug
: Allow users to print underlying debug information of accessor.Unpin
: Make sureAccessor
can be safely moved after being pinned, so users don’t need toPin<Box<A>>
.'static
: Make sureAccessor
is not a short-time reference, allow users to useAccessor
in closures and futures without playing with lifetime.
Implementer of Accessor
should take care of the following things:
- Implement
Debug
for backend, but don’t leak credentials. - Make sure the backend is
Send
andSync
, wrap the internal struct withArc<Mutex<T>>
if necessary.
§Associated Type
The first block of Access
trait is our associated types. We
require implementers to specify the type to be returned, thus avoiding
the additional overhead of dynamic dispatch.
Access
has four associated type so far:
Reader
: reader returned byread
operation.BlockingReader
: reader returned byblocking_read
operation.Lister
: lister returned bylist
operation.BlockingLister
: lister returned byblocking_scan
orblocking_list
operation.
Implementer of Accessor
should take care the following things:
- OpenDAL will erase those type at the final stage of Operator building. Please don’t return dynamic trait object like
oio::Reader
. - Use
()
as type if the operation is not supported.
§API Style
Every API of Access
follows the same style:
- All APIs have a unique
Operation
andCapability
- All APIs are orthogonal and do not overlap with each other
- Most APIs accept
path
andOpXxx
, and returnsRpXxx
. - Most APIs have
async
andblocking
variants, they share the same semantics but may have different underlying implementations.
Access
can declare their capabilities via AccessorInfo
’s set_capability
:
impl Access for MyBackend {
fn metadata(&self) -> AccessorInfo {
let mut am = AccessorInfo::default();
am.set_capability(
Capability {
read: true,
write: true,
..Default::default()
});
am.into()
}
}
Now that you have mastered Access
, let’s go and implement our own backend!
§Tutorial
This tutorial implements a duck
storage service that sends API
requests to a super-powered duck. Gagaga!
§Scheme
First of all, let’s pick a good Scheme
for our duck service. The
scheme should be unique and easy to understand. Normally we should
use its formal name.
For example, we will use s3
for AWS S3 Compatible Storage Service
instead of aws
or awss3
. This is because there are many storage
vendors that provide s3-like RESTful APIs, and our s3 service is
implemented to support all of them, not just AWS S3.
Obviously, we can use duck
as scheme, let’s add a new variant in Scheme
, and implement all required functions like Scheme::from_str
and Scheme::into_static
:
pub enum Scheme {
Duck,
}
§Builder
Then we can implement a builder for the duck service. The Builder
will provide APIs for users to configure, and they will create an
instance of a particular service.
Let’s create a backend
mod under services/duck
directory, and adding the following code.
use crate::raw::*;
use crate::*;
/// Duck Storage Service support. Gagaga!
///
/// # Capabilities
///
/// This service can be used to:
///
/// - [x] read
/// - [ ] write
/// - [ ] list
/// - [ ] presign
/// - [ ] blocking
///
/// # Configuration
///
/// - `root`: Set the work dir for backend.
///
/// ## Via Builder
///
/// ```no_run
/// use std::sync::Arc;
///
/// use anyhow::Result;
/// use opendal::services::Duck;
/// use opendal::Operator;
///
/// #[tokio::main]
/// async fn main() -> Result<()> {
/// // Create Duck backend builder.
/// let mut builder = Duck::default();
/// // Set the root for duck, all operations will happen under this root.
/// //
/// // NOTE: the root must be absolute path.
/// builder.root("/path/to/dir");
///
/// let op: Operator = Operator::new(builder)?.finish();
///
/// Ok(())
/// }
/// ```
#[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(default)]
#[non_exhaustive]
pub struct DuckConfig {
pub root: Option<String>,
}
#[derive(Default, Clone)]
pub struct DuckBuilder {
config: DuckConfig,
}
Note that DuckBuilder
is part of our public API, so it needs to be
documented. And any changes you make will directly affect users, so
please take it seriously. Otherwise, you will be hunted down by many
angry ducks.
Then, we can implement required APIs for DuckBuilder
:
impl DuckBuilder {
/// Set root of this backend.
///
/// All operations will happen under this root.
pub fn root(&mut self, root: &str) -> &mut Self {
self.config.root = if root.is_empty() {
None
} else {
Some(root.to_string())
};
self
}
}
impl Builder for DuckBuilder {
const SCHEME: Scheme = Scheme::Duck;
type Accessor = DuckBackend;
type Config = DuckConfig;
fn from_config(config: Self::Config) -> Self {
DuckBuilder { config: self }
}
fn build(self) -> Result<impl Access> {
debug!("backend build started: {:?}", &self);
let root = normalize_root(&self.config.root.clone().unwrap_or_default());
debug!("backend use root {}", &root);
Ok(DuckBackend { root })
}
}
DuckBuilder
is ready now, let’s try to play with real ducks!
§Backend
I’m sure you can see it already: DuckBuilder
will build a
DuckBackend
that implements Access
. The backend is what we used
to communicate with the super-powered ducks!
Let’s keep adding more code under backend.rs
:
/// Duck storage service backend
#[derive(Clone, Debug)]
pub struct DuckBackend {
root: String,
}
#[async_trait]
impl Access for DuckBackend {
type Reader = DuckReader;
type BlockingReader = ();
type Writer = ();
type BlockingWriter = ();
type Lister = ();
type BlockingLister = ();
fn metadata(&self) -> AccessorInfo {
let mut am = AccessorInfo::default();
am.set_scheme(Scheme::Duck)
.set_root(&self.root)
.set_capability(
Capability {
read: true,
..Default::default()
});
am.into()
}
async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
gagaga!()
}
}
Congratulations, we have implemented an Access
that can talk to
Super Power Ducks!
What!? There are no Super Power Ducks? So sad, but never mind, we have
really powerful storage services here. Welcome to pick one to implement. I promise you won’t
have to gagaga!()
this time.