Skip to main content

PostgresStorage

Struct PostgresStorage 

Source
pub struct PostgresStorage<Args> { /* private fields */ }
Expand description

A backend for persisting and consuming jobs behind a postgres database

§Features

FeatureStatusDescription
BackendSupports storage and retrieval of tasks
TaskSinkAbility to push new tasks
SerializationSerialization support for arguments
WorkflowFlexible enough to support workflows
Web InterfaceExpose a web interface for monitoring tasks
FetchByIdAllow fetching a task by its ID
RegisterWorkerAllow registering a worker with the backend
MakeSharedShare one connection across multiple workers via PostgresStorageFactory
WaitForCompletionWait for tasks to complete without blocking
ResumeByIdResume a task by its ID
ResumeAbandonedResume abandoned tasks
ListWorkersList all workers registered with the backend
ListTasksList all tasks in the backend

Key: ✅ : Supported | ⚠️ : Not implemented | ❌ : Not Supported | ❗ Limited support

Tests:
§Backend
#[tokio::main]
async fn main() {
    // let mut backend = /* snip */;
    
    async fn task(task: u32, worker: WorkerContext) {
        // Do some work 
    }
    let worker = WorkerBuilder::new("backend-test")
        .backend(backend)
        .build(task);
    let _ = worker.stream().take(1).collect::<Vec<_>>().await;
}
§TaskSink
#[tokio::main]
async fn main() {
    // let mut backend = /* snip */;
    
    backend.push(42).await.unwrap();

    async fn task(task: u32, worker: WorkerContext) {
        worker.stop().unwrap();
    }
    let worker = WorkerBuilder::new("task-sink-test")
        .backend(backend)
        .build(task);
    worker.run().await.unwrap();
}
§Serialization
#[tokio::main]
async fn main() {
    // let mut backend = /* snip */;
    
   fn assert_codec<B: BackendConfig + WireFormatBackend>(backend: B) 
   where
       B::Codec: Codec<B::Args, Compact=Vec<u8>>,
   {
   }
   assert_codec(backend);
}
§Workflow
#[tokio::main]
async fn main() {
    // let mut backend = /* snip */;
    
    backend.push(42).await.unwrap();

    async fn task1(task: u32, worker: WorkerContext) -> u32 {
        task + 99 
    }
    async fn task2(task: u32, worker: WorkerContext) -> u32 {
        task + 1 
    }
    async fn task3(task: u32, worker: WorkerContext) {
        assert_eq!(task, 142);
        worker.stop().unwrap();
    }
    let workflow = SteppedFlow::new("test-workflow")
       .and_then(task1)
       .and_then(task2)
       .and_then(task3);
    let worker = WorkerBuilder::new("workflow-test")
        .backend(backend)
        .build(workflow);
    worker.run().await.unwrap();
}
§WebUI
#[tokio::main]
async fn main() {
    // let mut backend = /* snip */;
    
    fn assert_web_ui<B: Expose<u32, Durable>>(backend: B) {};
    assert_web_ui(backend);
}
§WaitForCompletion
#[tokio::main]
async fn main() {
    // let mut backend = /* snip */;
    
    fn assert_wait_for_completion<B: WaitForCompletion<u32> + Backend>(backend: B) {};
    assert_wait_for_completion(backend);
}

Implementations§

Source§

impl PostgresStorage<()>

Source

pub async fn setup(pool: &PgPool) -> Result<(), Error>

Runs the PostgreSQL storage migrations.

§Fresh databases

No manual setup is required. Calling setup() will create the required tables and migration history.

§Upgrading to 1.0

⚠️ Important: Existing databases created by a pre-1.0 version require a one-time manual migration before calling setup().

The 1.0 migration history is no longer relocated automatically by setup(). Follow the “Upgrading to 1.0” section in the README to perform the required transition.

After the transition has been completed, setup() can be used normally for subsequent migrations.

§Example
use apalis_postgres::PostgresStorage;
use sqlx::PgPool;

PostgresStorage::<()>::setup(&pool).await?;
§Errors

Returns an error if the migrations cannot be applied to the database.

Source

pub fn migrations() -> Migrator

Get postgres migrations without running them

Source§

impl<Args> PostgresStorage<Args>

Source

pub fn new(pool: &PgPool) -> Self

Creates a new PostgresStorage instance.

Source

pub fn with_pubsub(self) -> PollWith<Self, StreamStrategy<Pubsub>>

Mount a standalone Pubsub which uses its own connection under the hood

Source

pub fn with_config(self, config: Config) -> Self

Configure a new PostgresStorage instance.

Source

pub fn pool(&self) -> &PgPool

Returns a reference to the pool.

Source

pub fn config(&self) -> &Config

Returns a reference to the config.

Trait Implementations§

Source§

impl<Args> Backend for PostgresStorage<Args>

Source§

type Task = Task<Vec<u8>>

The type of task the backend emits.
Source§

type Error = Error

The error type returned by backend operations
Source§

fn poll_ready( &mut self, cx: &mut Context<'_>, worker: &WorkerContext, ) -> Poll<Result<(), Self::Error>>

Polls whether the backend is ready for the worker to request more work. Read more
Source§

fn poll_next( &mut self, cx: &mut Context<'_>, worker: &WorkerContext, ) -> Poll<Option<Result<PgTask, Self::Error>>>

Polls the backend for the next available task for this worker. Read more
Source§

fn poll_close( &mut self, cx: &mut Context<'_>, worker: &WorkerContext, ) -> Poll<Result<(), Self::Error>>

Flushes/releases any resources the backend holds (pending acks, open subscriptions, connections) before the worker fully shuts down.
Source§

impl<Args> BackendConfig for PostgresStorage<Args>

Source§

type Args = Args

The type of argument this backend emits
Source§

type Kind = Durable

This defines the kind of Backend
Source§

type Id = Ulid

The internal type of this Backend’s TaskId
Source§

type Config = Config

The config for the backend
Source§

type Layer = TaskPersistLayer<JsonCodec<Value>, Value>

The type representing backend middleware layer.
Source§

fn config(&self) -> &Self::Config

Returns the config associated with the backend.
Source§

fn middleware(&mut self, _: &mut WorkerContext) -> Self::Layer

Returns the backend’s middleware layer.
Source§

impl<Args> Clone for PostgresStorage<Args>

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<Args> FetchById for PostgresStorage<Args>
where Self: Backend<Error = Error, Task = PgTask>, Args: 'static,

Source§

fn fetch_by_id( &mut self, id: &PgTaskId, ) -> impl Future<Output = Result<Option<Self::Task>, Self::Error>> + Send

Fetch a task by its unique identifier
Source§

impl<Args> ListAllTasks for PostgresStorage<Args>
where PostgresStorage<Args>: Backend<Error = Error>,

Source§

fn list_all_tasks( &self, filter: &Filter, ) -> impl Future<Output = Result<Vec<Task<Self::Compact>>, Self::Error>> + Send

List tasks matching the given filter in all queues
Source§

impl<Args> ListQueues for PostgresStorage<Args>
where PostgresStorage<Args>: Backend<Error = Error>,

Source§

fn list_queues( &self, ) -> impl Future<Output = Result<Vec<QueueInfo>, Self::Error>> + Send

List all available queues in the backend
Source§

impl<Args> ListTasks for PostgresStorage<Args>
where PostgresStorage<Args>: Backend<Error = Error>, Args: 'static,

Source§

fn list_tasks( &self, filter: &Filter, ) -> impl Future<Output = Result<Vec<PgTask>, Self::Error>> + Send

List tasks matching the given filter in the current queue
Source§

impl<Args: Sync> ListWorkers for PostgresStorage<Args>
where PostgresStorage<Args>: Backend<Error = Error>,

Source§

fn list_workers( &self, ) -> impl Future<Output = Result<Vec<RunningWorker>, Self::Error>> + Send

List all registered workers in the current queue
Source§

fn list_all_workers( &self, ) -> impl Future<Output = Result<Vec<RunningWorker>, Self::Error>> + Send

List all registered workers in all queues
Source§

impl<Args> Metrics for PostgresStorage<Args>
where PostgresStorage<Args>: Backend<Error = Error>,

Source§

fn global( &self, ) -> impl Future<Output = Result<Vec<Statistic>, Self::Error>> + Send

Collects and returns global statistics from the backend
Source§

fn fetch_by_queue( &self, ) -> impl Future<Output = Result<Vec<Statistic>, Self::Error>> + Send

Collects and returns statistics for a specific queue
Source§

impl<Args> Sink<Task<Vec<u8>>> for PostgresStorage<Args>
where Args: Unpin + Send + Sync + 'static,

Source§

type Error = Error

The type of value produced by the sink when an error occurs.
Source§

fn poll_ready( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll<Result<(), Self::Error>>

Attempts to prepare the Sink to receive a value. Read more
Source§

fn start_send(self: Pin<&mut Self>, item: PgTask) -> Result<(), Self::Error>

Begin the process of sending a value to the sink. Each call to this function must be preceded by a successful call to poll_ready which returned Poll::Ready(Ok(())). Read more
Source§

fn poll_flush( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll<Result<(), Self::Error>>

Flush any remaining output from this sink. Read more
Source§

fn poll_close( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll<Result<(), Self::Error>>

Flush any remaining output and close this sink, if necessary. Read more
Source§

impl<'pin, Args> Unpin for PostgresStorage<Args>
where PinnedFieldsOf<__PostgresStorage<'pin, Args>>: Unpin,

Source§

impl<O: 'static + Send, Args> WaitForCompletion<O> for PostgresStorage<Args>

Source§

type ResultStream = Pin<Box<dyn Stream<Item = Result<TaskResult<O>, <PostgresStorage<Args> as Backend>::Error>> + Send>>

The result stream type yielding task results
Source§

fn wait_for( &self, task_ids: impl IntoIterator<Item = TaskId>, ) -> Self::ResultStream

Wait for multiple tasks to complete, yielding results as they become available
Source§

fn check_status( &self, task_ids: impl IntoIterator<Item = TaskId> + Send, ) -> impl Future<Output = Result<Vec<TaskResult<O>>, Self::Error>> + Send

Check current status of tasks without waiting
Source§

fn wait_for_single(&self, task_id: TaskId) -> Self::ResultStream

Wait for a single task to complete, yielding its result
Source§

impl<Args> WireFormatBackend for PostgresStorage<Args>

Source§

type Codec = JsonCodec

The codec used to encode and decode tasks.
Source§

type Compact = Vec<u8>

The compact representation of task arguments.
Source§

fn codec(&self) -> &Self::Codec

Returns a reference to the codec used by the backend.

Auto Trait Implementations§

§

impl<Args> !Freeze for PostgresStorage<Args>

§

impl<Args> !RefUnwindSafe for PostgresStorage<Args>

§

impl<Args> !UnwindSafe for PostgresStorage<Args>

§

impl<Args> Send for PostgresStorage<Args>
where PhantomData<Args>: Send,

§

impl<Args> Sync for PostgresStorage<Args>
where PhantomData<Args>: Sync,

§

impl<Args> UnsafeUnpin for PostgresStorage<Args>
where PhantomData<Args>: UnsafeUnpin,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<B> BackendExt for B
where B: Backend,

Source§

fn poll_next_args( &mut self, cx: &mut Context<'_>, worker: &WorkerContext, ) -> Poll<Option<Result<Task<Self::Args>, PollNextArgsError<Self>>>>
where Self: Sized + BackendConfig + WireFormatBackend + Backend<Task = Task<Self::Compact>>, Self::Codec: Codec<Self::Args, Compact = Self::Compact>, <Self::Codec as Codec<Self::Args>>::Error: Error + Send + Sync + 'static,

A convenience method for calling poll_next and decoding the Args in one step, returning a Task<Self::Args, ..> instead of Task<Self::Compact, ..>.
Source§

fn pipe_to<Dst>(self, backend: Dst) -> Pipe<Dst, Self>
where Self: Sized,

Pipes every task polled from this backend into sink Read more
Source§

fn inspect_err<F>(self, f: F) -> InspectErr<Self, F>
where Self: Sized, F: Fn(&Self::Error),

Attaches a callback F to be run on each error produced while polling the backend.
Source§

fn map_err<F, E2>(self, f: F) -> MapErr<Self, F>
where Self: Sized, F: Fn(Self::Error) -> E2,

Maps errors produced by the backend from Self::Error into E2, useful for heterogeneous composed backends.
Source§

fn with_codec<NewCodec>(self, codec: NewCodec) -> WithCodec<Self, NewCodec>
where Self: Sized + BackendConfig, NewCodec: Codec<Self::Args>,

Swaps out the backend’s serialization codec entirely (JSON, MessagePack, Protobuf, …) without touching storage logic.
Source§

fn poll_with_stream<S>(self, stream: S) -> PollWith<Self, StreamStrategy<S>>
where Self: Sized, S: Stream + Unpin + Send + 'static,

Wake the worker when a stream receives a new item
Source§

fn poll_with_interval( self, duration: Duration, ) -> PollWith<Self, IntervalStrategy>
where Self: Sized,

Wake the worker periodically
Source§

fn poll_with_backoff( self, interval: Duration, config: BackoffConfig, ) -> PollWith<Self, BackoffStrategy>
where Self: Sized,

Wake the worker periodically with a backoff
Source§

fn poll_with_strategy<S>(self, strategy: S) -> PollWith<Self, S>
where Self: Sized, S: PollStrategy,

Wake the worker with a custom strategy
Source§

fn before_start<F, Fut>(self, f: F) -> BeforeStart<Self, Self::Error>
where Self: Sized, F: Fn(&mut Self) -> Fut + Send + Sync + 'static, Fut: Future<Output = Result<(), Self::Error>> + Send + 'static,

Runs an async callback once, before the backend’s first poll_ready is delegated.
Source§

fn before_stop<F, Fut>(self, f: F) -> BeforeStop<Self, Self::Error>
where Self: Sized, F: Fn(&mut Self) -> Fut + Send + Sync + 'static, Fut: Future<Output = Result<(), Self::Error>> + Send + 'static,

Runs an async callback once, before the backend’s poll_close is called.
Source§

fn after_start<F, Fut>(self, f: F) -> AfterStart<Self, Self::Error>
where Self: Sized, F: for<'c> Fn(&mut Self) -> Fut + for<'c> Send + for<'c> Sync + 'static, Fut: Future<Output = Result<(), Self::Error>> + Send + 'static,

Runs an async callback once, after the backend’s first poll_ready is successful.
Source§

fn after_stop<F, Fut>(self, f: F) -> AfterStop<Self, Self::Error>
where Self: Sized, F: Fn(&mut Self) -> Fut + Send + Sync + 'static, Fut: Future<Output = Result<(), Self::Error>> + Send + 'static,

Runs an async callback once, after the worker has stopped and backend has cleaned up.
Source§

fn interleave<S>(self, stream: S) -> Interleave<Self, S>
where Self: Sized, S: Stream<Item = Result<Self::Task, Self::Error>> + Unpin,

Interleaves the external stream with the backend.
Source§

fn wake_on_push(self) -> WakeOnPush<Self>
where Self: Sized,

Wakes the worker when a new item is pushed
Source§

fn shared(self) -> Shared<Self>
where Self: WireFormatBackend + Send, Self::Codec: Clone,

Create a cloneable handle to the inner backend where all handles are clone.
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<B, Args, Kind> Expose<Args, Kind> for B

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, Item> SinkExt<Item> for T
where T: Sink<Item> + ?Sized,

Source§

fn with<U, Fut, F, E>(self, f: F) -> With<Self, Item, U, Fut, F>
where F: FnMut(U) -> Fut, Fut: Future<Output = Result<Item, E>>, E: From<Self::Error>, Self: Sized,

Composes a function in front of the sink. Read more
Source§

fn with_flat_map<U, St, F>(self, f: F) -> WithFlatMap<Self, Item, U, St, F>
where F: FnMut(U) -> St, St: Stream<Item = Result<Item, Self::Error>>, Self: Sized,

Composes a function in front of the sink. Read more
Source§

fn sink_map_err<E, F>(self, f: F) -> SinkMapErr<Self, F>
where F: FnOnce(Self::Error) -> E, Self: Sized,

Transforms the error returned by the sink.
Source§

fn sink_err_into<E>(self) -> SinkErrInto<Self, Item, E>
where Self: Sized, Self::Error: Into<E>,

Map this sink’s error to a different error type using the Into trait. Read more
Source§

fn buffer(self, capacity: usize) -> Buffer<Self, Item>
where Self: Sized,

Adds a fixed-size buffer to the current sink. Read more
Source§

fn close(&mut self) -> Close<'_, Self, Item>
where Self: Unpin,

Close the sink.
Source§

fn fanout<Si>(self, other: Si) -> Fanout<Self, Si>
where Self: Sized, Item: Clone, Si: Sink<Item, Error = Self::Error>,

Fanout items to multiple sinks. Read more
Source§

fn flush(&mut self) -> Flush<'_, Self, Item>
where Self: Unpin,

Flush the sink, processing all pending items. Read more
Source§

fn send(&mut self, item: Item) -> Send<'_, Self, Item>
where Self: Unpin,

A future that completes after the given item has been fully processed into the sink, including flushing. Read more
Source§

fn feed(&mut self, item: Item) -> Feed<'_, Self, Item>
where Self: Unpin,

A future that completes after the given item has been received by the sink. Read more
Source§

fn send_all<'a, St>(&'a mut self, stream: &'a mut St) -> SendAll<'a, Self, St>
where St: TryStream<Ok = Item, Error = Self::Error> + Stream + Unpin + ?Sized, Self: Unpin,

A future that completes after the given stream has been fully processed into the sink, including flushing. Read more
Source§

fn left_sink<Si2>(self) -> Either<Self, Si2>
where Si2: Sink<Item, Error = Self::Error>, Self: Sized,

Wrap this sink in an Either sink, making it the left-hand variant of that Either. Read more
Source§

fn right_sink<Si1>(self) -> Either<Si1, Self>
where Si1: Sink<Item, Error = Self::Error>, Self: Sized,

Wrap this stream in an Either stream, making it the right-hand variant of that Either. Read more
Source§

fn poll_ready_unpin( &mut self, cx: &mut Context<'_>, ) -> Poll<Result<(), Self::Error>>
where Self: Unpin,

A convenience method for calling Sink::poll_ready on Unpin sink types.
Source§

fn start_send_unpin(&mut self, item: Item) -> Result<(), Self::Error>
where Self: Unpin,

A convenience method for calling Sink::start_send on Unpin sink types.
Source§

fn poll_flush_unpin( &mut self, cx: &mut Context<'_>, ) -> Poll<Result<(), Self::Error>>
where Self: Unpin,

A convenience method for calling Sink::poll_flush on Unpin sink types.
Source§

fn poll_close_unpin( &mut self, cx: &mut Context<'_>, ) -> Poll<Result<(), Self::Error>>
where Self: Unpin,

A convenience method for calling Sink::poll_close on Unpin sink types.
Source§

impl<Args, S, E, C> TaskSink<Args, Durable> for S
where S: Sink<Task<<C as Codec<Args>>::Compact>, Error = E> + Unpin + Backend<Error = E> + WireFormatBackend<Codec = C> + BackendConfig<Args = Args, Kind = Durable> + Send, Args: Send, <C as Codec<Args>>::Compact: Send, C: Codec<Args> + Clone + Send + Sync, E: Send, <C as Codec<Args>>::Error: Error + Send + Sync + 'static,

Source§

fn start_send( self: Pin<&mut S>, item: Task<Args>, ) -> Result<(), TaskSinkError<<S as Backend>::Error>>

Begins sending a task to the sink. Read more
Source§

fn poll_ready( self: Pin<&mut S>, cx: &mut Context<'_>, ) -> Poll<Result<(), TaskSinkError<<S as Backend>::Error>>>

Polls the sink until it is ready to accept another task. Read more
Source§

fn poll_flush( self: Pin<&mut S>, cx: &mut Context<'_>, ) -> Poll<Result<(), TaskSinkError<<S as Backend>::Error>>>

Polls the sink until all previously submitted tasks have been flushed. Read more
Source§

fn poll_close( self: Pin<&mut S>, cx: &mut Context<'_>, ) -> Poll<Result<(), TaskSinkError<<S as Backend>::Error>>>

Polls the sink until it has been closed. Read more
Source§

async fn push( &mut self, task: Args, ) -> Result<(), TaskSinkError<<S as Backend>::Error>>

Pushes a single task into the backend. Read more
Source§

async fn push_bulk( &mut self, tasks: Vec<Args>, ) -> Result<(), TaskSinkError<<S as Backend>::Error>>

Pushes multiple tasks into the backend. Read more
Source§

async fn push_stream( &mut self, tasks: impl Stream<Item = Args> + Unpin + Send, ) -> Result<(), TaskSinkError<<S as Backend>::Error>>

Pushes tasks from a stream into the backend. Read more
Source§

async fn push_task( &mut self, task: Task<Args>, ) -> Result<(), TaskSinkError<<S as Backend>::Error>>

Pushes a fully constructed task into the backend. Read more
Source§

async fn push_all( &mut self, tasks: impl Stream<Item = Task<Args>> + Unpin + Send, ) -> Result<(), TaskSinkError<<S as Backend>::Error>>

Pushes fully constructed tasks from a stream into the backend. Read more
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more