Skip to content

fix(etl): skip errored state and interrupt flush wait on table-copy shutdown - #881

Open
abhizer wants to merge 1 commit into
supabase:mainfrom
abhizer:fix/table-copy-shutdown-write-race
Open

fix(etl): skip errored state and interrupt flush wait on table-copy shutdown#881
abhizer wants to merge 1 commit into
supabase:mainfrom
abhizer:fix/table-copy-shutdown-write-race

Conversation

@abhizer

@abhizer abhizer commented Jul 7, 2026

Copy link
Copy Markdown

Currently, during table-copy, if the destination drops the AsyncResult while the pipeline is shutting down, the table ends up in an Errored state. But dropping the AsyncResult during shutdown should be safe: on restart, we should just ensure the retry copies the table from the start.

Also make the copy loop stop waiting on the flush result once shutdown fires, instead of blocking on a destination that never completes it.

What kind of change does this PR introduce?

Bug fix

What is the current behavior?

During table-copy, if the destination drops the AsyncResult while the
pipeline is shutting down, the table ends up in an Errored state. The
copy loop also waits on the flush result unconditionally, so it can
hang if the destination never completes it.

What is the new behavior?

Dropping the AsyncResult during shutdown no longer persists an Errored
state; the table retries the copy from the start on restart. The copy
loop also stops waiting on the flush result once shutdown fires.

@abhizer
abhizer requested a review from a team as a code owner July 7, 2026 14:39
@iambriccardo

Copy link
Copy Markdown
Contributor

Hi, thanks for this PR! I will get to it once I find the time.

@abhizer

abhizer commented Jul 12, 2026

Copy link
Copy Markdown
Author

Thanks!

…hutdown

Currently, during table-copy, if the destination drops the AsyncResult
while the pipeline is shutting down, the table ends up in an Errored
state. But dropping the AsyncResult during shutdown should be safe:
on restart, we should just ensure the retry copies the table from the
start.

Also make the copy loop stop waiting on the flush result once shutdown
fires, instead of blocking on a destination that never completes it.

Signed-off-by: Abhinav Gyawali <22275402+abhizer@users.noreply.github.com>
@abhizer
abhizer force-pushed the fix/table-copy-shutdown-write-race branch from 5a9d35f to 6d33c24 Compare July 13, 2026 09:25
@bnjjj

bnjjj commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Hey @abhizer I think I got this issue last week too. What was the error you got once it happened ?

@coveralls

Copy link
Copy Markdown

Coverage Status

Coverage is 70.861%abhizer:fix/table-copy-shutdown-write-race into supabase:main. No base build found for supabase:main.

// a stored `Errored` state is never retried across restarts, so the
// table would otherwise stall on every later run. A dropped sender also
// counts as shutdown.
if shutdown_rx.has_changed().unwrap_or(true) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the correct solution for a concurrent system like ETL. Having the shutdown being true at this phase, doesn't guarantee us that the error will be caused by the shutdown procedure.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. We'd just like to be able to drop the acknowledgements once we've initiated the shutdown.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give me more context on your ETL use case? So that I can see how we can better design shutdown in case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason of why I am saying this is that, the shutdown on the destination is called on purpose after all of ETL is done, so that we can stop producing data on our end and then the destination can perform teardown.

The failure should not happen because a destination is technically unaware of shutdown until ETL won't be interested anymore about the result. And if the result is sent back and the channel is closed a warning will be raised.

if tx.send(result).is_err() {
    warn!("could not send async result because receiver was already closed");
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our use case is the Postgres CDC input connector for feldera.

We want the connector to snapshot, and then follow the cdc stream, this works.

But we'd also like to ensure fault tolerance and be able to reingest the table in cases of failure. In cases of failure, the connector sees that the pipeline is shutting down, and triggers the etl pipeline to shutdown as well. And in such cases, returns from write_table_rows without acknowledging the AsyncResult, which can lead to the table being in errored state.

Because we've already requested a shutdown here, we want this to be okay.

Related: The Accept / Durable api for table copy, currently on Accept can currently allow the table to transition to Ready, causing etl to skip reingesting on restart. I do not think that Accept should have this durable side effect.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, I get what you mean. The thing here is that we need to clarify the semantics. I feel like from ETL's side, if a channel is closed without a response, it's a problem. The destination should take care of sending back a response. Maybe we could classify a response as "gracefully stop". But from my idea, I would like the system to be like, if there is shutdown after the write_table_rows method, we immediately return. Would that work?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't think I quite understand the immediately return model. Do you mean something like a biased tokio select, on shutdown.changed() and the write_table_rows method?

But yes, the gracefully stop idea, for shutdown during write_table_rows, returning an ErrorKind::DestinationShutdown should work well for us, and in other similar use cases. ETL could then treat it as a graceful cancellation, instead of an error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question I would have is. I assume your write_table_rows fails because you have an out-of-bound shutdown signal receiver in your destination which makes it stop and not return a result?

The reason I am asking is that ETL is designed in a way where the shutdown procedure of a destination should be made in the shutdown() method, so that teardown is properly controlled.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question I would have is. I assume your write_table_rows fails because you have an out-of-bound shutdown signal receiver in your destination which makes it stop and not return a result?

Yes. As the connector is part of the pipeline, the shutdown procedure cannot quite be contained in the shutdown() method. We need to shutdown the etl pipeline safely, when shutting down the outer pipeline.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would make the implementation quite a bit uglier, is this a problem also for the apply loop?

.write_table_rows(&replicated_table_schema, table_rows, flush_result)
.await?;
let write_status = pending_flush_result.await.into_result()?;
let write_status = tokio::select! {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fair but I think it would be better if we make an abstraction which behind the scenes waits for shutdown on a result.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am working now on a PR to do that.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#900 this is the PR in progress btw

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix!

@abhizer

abhizer commented Jul 15, 2026

Copy link
Copy Markdown
Author

I think I got this issue last week too. What was the error you got once it happened ?

@bnjjj I don't exactly remember, but it was something to do with the table being in errored state, when we've tried to shutdown the etl pipeline, and then dropped the async result.

@iambriccardo

Copy link
Copy Markdown
Contributor

I think I got this issue last week too. What was the error you got once it happened ?

@bnjjj I don't exactly remember, but it was something to do with the table being in errored state, when we've tried to shutdown the etl pipeline, and then dropped the async result.

This is interesting. The only way in which it could have happened is if the AsyncResult was dropped by the destination while it was waiting on it. At least this is what I am thinking of.

@bnjjj

bnjjj commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Yes probably something in the ducklake destination I'll check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants