forked from tribes/guix
2c51b803e3
* gnu/packages/patches/zed-0.225.10-add-guix-container-support.patch: New file. * gnu/packages/patches/zed-0.225.10-collapse-multiline-git-deps.patch: New file. * gnu/packages/patches/zed-0.225.10-disable-dlopen.patch: New file. * gnu/packages/patches/zed-0.225.10-exclude-libwebrtc-from-audio.patch: New file. * gnu/packages/patches/zed-0.225.10-fix-sqlite-memory-mode.patch: New file. * gnu/packages/patches/zed-0.225.10-fix-test-db-isolation.patch: New file. * gnu/packages/patches/zed-0.225.10-fix-workspace-race.patch: New file. * gnu/packages/patches/zed-0.225.10-keep-regular-file-workspaces.patch: New file. * gnu/packages/patches/zed-0.225.10-remove-patch-crates-io.patch: New file. * gnu/packages/patches/zed-0.225.10-use-mock-livekit-on-linux.patch: New file. * gnu/packages/patches/rust-candle-0.9.1-add-candle-onnx-to-workspace.patch: New file. * gnu/local.mk (dist_patch_DATA): Register them. * gnu/packages/rust-sources.scm (rust-alacritty-0.25.1.9d9640d, rust-candle-0.9.1.724d75e, rust-dap-types-0.0.1.1b461b3, rust-gh-workflow-0.8.0.c9eac0e, rust-livekit-0.7.8.5f04705, rust-notify-8.2.0.ce58c24, rust-pet-0.1.0.d5b5bb0, rust-tiktoken-rs-0.9.1.2570c43, rust-zed-xim-0.4.0-zed.16f35a2): New variables. * gnu/packages/rust-crates.scm (lookup-cargo-inputs): Modify. * gnu/packages/text-editors.scm (zed): New variable. Change-Id: I16d4c5431e3398261ac4eb74483747c09cf74449
311 lines
12 KiB
Diff
311 lines
12 KiB
Diff
Author: Danny Milosavljevic <dannym@friendly-machines.com>
|
|
Subject: Fix workspace persistence races during restoration.
|
|
Date: 2026-03-01
|
|
License: expat
|
|
|
|
In 0.225.10, workspace persistence has three separate ordering problems.
|
|
|
|
First, newly allocated workspace IDs could be used for item persistence before a
|
|
parent row had been inserted into the workspaces table. That made editor
|
|
persistence race with workspace creation and could fail with a foreign key
|
|
constraint error.
|
|
|
|
Second, workspace layout persistence and per-item persistence run on separate
|
|
asynchronous paths. That allowed the workspace layout to be saved with item IDs
|
|
before the background item-serialization worker had finished writing the
|
|
corresponding per-item rows. On restore, deserializers that require persisted
|
|
item state could then fail with "No entry in database for item_id ...", which
|
|
could drop restored state and break restoration features that depend on that
|
|
persisted item state, including edited-window restoration.
|
|
|
|
Third, restore-side cleanup assumed that deserialized items had already been
|
|
saved under their new runtime item IDs after being added to panes. In reality,
|
|
adding a deserialized item only enqueues that serialization work. Cleanup could
|
|
therefore delete the old persisted rows before the new rows existed, leaving the
|
|
next reopen with a pane/item layout that still referenced state that had just
|
|
been deleted.
|
|
|
|
This change fixes all three issues:
|
|
|
|
- eagerly create the parent workspaces row as soon as a new workspace ID is
|
|
allocated, before any item serializer can write child rows
|
|
- keep item persistence on the existing serialization queue, but add an
|
|
ordering barrier before saving workspace layout so the layout is never
|
|
published ahead of the item state it references
|
|
- during restore, wait for the queued serialization of newly added
|
|
deserialized items before cleaning up stale persisted item IDs, and skip
|
|
that cleanup pass if the barrier cannot be established or awaited so stale
|
|
rows are kept rather than deleting still-needed state
|
|
|
|
See also <https://github.com/zed-industries/zed/pull/50873>.
|
|
|
|
diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs
|
|
index e0ad046e8d..aeb4c3ba39 100644
|
|
--- a/crates/workspace/src/persistence.rs
|
|
+++ b/crates/workspace/src/persistence.rs
|
|
@@ -2124,6 +2124,13 @@ impl WorkspaceDb {
|
|
}
|
|
}
|
|
|
|
+ query! {
|
|
+ pub(crate) async fn ensure_workspace_row(workspace_id: WorkspaceId) -> Result<()> {
|
|
+ INSERT OR IGNORE INTO workspaces(workspace_id)
|
|
+ VALUES (?1)
|
|
+ }
|
|
+ }
|
|
+
|
|
query! {
|
|
pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
|
|
UPDATE workspaces
|
|
diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs
|
|
index 4141952cc7..3ea631ed6f 100644
|
|
--- a/crates/workspace/src/workspace.rs
|
|
+++ b/crates/workspace/src/workspace.rs
|
|
@@ -1175,6 +1175,11 @@ enum WorkspaceLocation {
|
|
None,
|
|
}
|
|
|
|
+enum SerializableItemMessage {
|
|
+ Item(Box<dyn SerializableItemHandle>),
|
|
+ Barrier(oneshot::Sender<()>),
|
|
+}
|
|
+
|
|
type PromptForNewPath = Box<
|
|
dyn Fn(
|
|
&mut Workspace,
|
|
@@ -1253,7 +1258,7 @@ pub struct Workspace {
|
|
on_prompt_for_open_path: Option<PromptForOpenPath>,
|
|
terminal_provider: Option<Box<dyn TerminalProvider>>,
|
|
debugger_provider: Option<Arc<dyn DebuggerProvider>>,
|
|
- serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
|
|
+ serializable_items_tx: UnboundedSender<SerializableItemMessage>,
|
|
_items_serializer: Task<Result<()>>,
|
|
session_id: Option<String>,
|
|
scheduled_tasks: Vec<Task<()>>,
|
|
@@ -1554,7 +1559,7 @@ impl Workspace {
|
|
}
|
|
|
|
let (serializable_items_tx, serializable_items_rx) =
|
|
- mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
|
|
+ mpsc::unbounded::<SerializableItemMessage>();
|
|
let _items_serializer = cx.spawn_in(window, async move |this, cx| {
|
|
Self::serialize_items(&this, serializable_items_rx, cx).await
|
|
});
|
|
@@ -1759,6 +1764,7 @@ impl Workspace {
|
|
} else {
|
|
DB.next_id().await.unwrap_or_else(|_| Default::default())
|
|
};
|
|
+ DB.ensure_workspace_row(workspace_id).await?;
|
|
|
|
let toolchains = DB.toolchains(workspace_id).await?;
|
|
|
|
@@ -6036,8 +6042,16 @@ impl Workspace {
|
|
window_id: Some(window.window_handle().window_id().as_u64()),
|
|
user_toolchains,
|
|
};
|
|
+ let item_serialization_barrier = self.item_serialization_barrier();
|
|
|
|
window.spawn(cx, async move |_| {
|
|
+ let Some(item_serialization_barrier) = item_serialization_barrier.log_err()
|
|
+ else {
|
|
+ return;
|
|
+ };
|
|
+ if item_serialization_barrier.await.log_err().is_none() {
|
|
+ return;
|
|
+ }
|
|
persistence::DB.save_workspace(serialized_workspace).await;
|
|
})
|
|
}
|
|
@@ -6108,31 +6122,49 @@ impl Workspace {
|
|
|
|
async fn serialize_items(
|
|
this: &WeakEntity<Self>,
|
|
- items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
|
|
+ mut items_rx: UnboundedReceiver<SerializableItemMessage>,
|
|
cx: &mut AsyncWindowContext,
|
|
) -> Result<()> {
|
|
const CHUNK_SIZE: usize = 200;
|
|
|
|
- let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
|
|
+ while let Some(message) = items_rx.next().await {
|
|
+ let mut unique_items = HashMap::default();
|
|
+ let mut barrier = None;
|
|
+ let mut channel_closed = false;
|
|
|
|
- while let Some(items_received) = serializable_items.next().await {
|
|
- let unique_items =
|
|
- items_received
|
|
- .into_iter()
|
|
- .fold(HashMap::default(), |mut acc, item| {
|
|
- acc.entry(item.item_id()).or_insert(item);
|
|
- acc
|
|
- });
|
|
+ Self::push_serializable_item_message(message, &mut unique_items, &mut barrier);
|
|
|
|
- // We use into_iter() here so that the references to the items are moved into
|
|
- // the tasks and not kept alive while we're sleeping.
|
|
- for (_, item) in unique_items.into_iter() {
|
|
- if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
|
|
- item.serialize(workspace, false, window, cx)
|
|
- }) {
|
|
- cx.background_spawn(async move { task.await.log_err() })
|
|
- .detach();
|
|
+ while barrier.is_none() && unique_items.len() < CHUNK_SIZE {
|
|
+ match items_rx.try_next() {
|
|
+ Ok(Some(message)) => {
|
|
+ Self::push_serializable_item_message(
|
|
+ message,
|
|
+ &mut unique_items,
|
|
+ &mut barrier,
|
|
+ );
|
|
+ }
|
|
+ Ok(None) => {
|
|
+ channel_closed = true;
|
|
+ break;
|
|
+ }
|
|
+ Err(_) => break,
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let batch_succeeded = Self::serialize_item_batch(this, unique_items, cx)
|
|
+ .await
|
|
+ .log_err()
|
|
+ .is_some();
|
|
+
|
|
+ if let Some(barrier) = barrier {
|
|
+ if batch_succeeded {
|
|
+ let _ = barrier.send(());
|
|
}
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if channel_closed {
|
|
+ break;
|
|
}
|
|
|
|
cx.background_executor()
|
|
@@ -6143,12 +6175,54 @@ impl Workspace {
|
|
Ok(())
|
|
}
|
|
|
|
+ fn push_serializable_item_message(
|
|
+ message: SerializableItemMessage,
|
|
+ unique_items: &mut HashMap<EntityId, Box<dyn SerializableItemHandle>>,
|
|
+ barrier: &mut Option<oneshot::Sender<()>>,
|
|
+ ) {
|
|
+ match message {
|
|
+ SerializableItemMessage::Item(item) => {
|
|
+ unique_items.entry(item.item_id()).or_insert(item);
|
|
+ }
|
|
+ SerializableItemMessage::Barrier(sender) => {
|
|
+ *barrier = Some(sender);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ async fn serialize_item_batch(
|
|
+ this: &WeakEntity<Self>,
|
|
+ unique_items: HashMap<EntityId, Box<dyn SerializableItemHandle>>,
|
|
+ cx: &mut AsyncWindowContext,
|
|
+ ) -> Result<()> {
|
|
+ let serialize_tasks = match this.update_in(cx, move |workspace, window, cx| {
|
|
+ unique_items
|
|
+ .into_values()
|
|
+ .filter_map(|item| item.serialize(workspace, false, window, cx))
|
|
+ .collect::<Vec<_>>()
|
|
+ }) {
|
|
+ Ok(serialize_tasks) => serialize_tasks,
|
|
+ Err(_) => return Ok(()),
|
|
+ };
|
|
+
|
|
+ try_join_all(serialize_tasks).await?;
|
|
+ Ok(())
|
|
+ }
|
|
+
|
|
+ fn item_serialization_barrier(&self) -> Result<oneshot::Receiver<()>> {
|
|
+ let (sender, receiver) = oneshot::channel();
|
|
+ self.serializable_items_tx
|
|
+ .unbounded_send(SerializableItemMessage::Barrier(sender))
|
|
+ .map_err(|err| anyhow!("failed to send item serialization barrier: {err}"))?;
|
|
+ Ok(receiver)
|
|
+ }
|
|
+
|
|
pub(crate) fn enqueue_item_serialization(
|
|
&mut self,
|
|
item: Box<dyn SerializableItemHandle>,
|
|
) -> Result<()> {
|
|
self.serializable_items_tx
|
|
- .unbounded_send(item)
|
|
+ .unbounded_send(SerializableItemMessage::Item(item))
|
|
.map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
|
|
}
|
|
|
|
@@ -6252,24 +6326,42 @@ impl Workspace {
|
|
// after loading the items, we might have different items and in order to avoid
|
|
// the database filling up, we delete items that haven't been loaded now.
|
|
//
|
|
- // The items that have been loaded, have been saved after they've been added to the workspace.
|
|
- let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
|
|
- item_ids_by_kind
|
|
- .into_iter()
|
|
- .map(|(item_kind, loaded_items)| {
|
|
- SerializableItemRegistry::cleanup(
|
|
- item_kind,
|
|
- serialized_workspace.id,
|
|
- loaded_items,
|
|
- window,
|
|
- cx,
|
|
- )
|
|
- .log_err()
|
|
- })
|
|
- .collect::<Vec<_>>()
|
|
- })?;
|
|
+ // Newly deserialized items only enqueue serialization when they are added to their panes.
|
|
+ // Wait for that queued work to finish before deleting the old persisted item ids.
|
|
+ let item_serialization_barrier = workspace
|
|
+ .update_in(cx, |workspace, _, _| {
|
|
+ workspace.item_serialization_barrier().log_err()
|
|
+ })
|
|
+ .log_err()
|
|
+ .flatten();
|
|
+
|
|
+ let item_serialization_flushed = if let Some(item_serialization_barrier) =
|
|
+ item_serialization_barrier
|
|
+ {
|
|
+ item_serialization_barrier.await.log_err().is_some()
|
|
+ } else {
|
|
+ false
|
|
+ };
|
|
|
|
- futures::future::join_all(clean_up_tasks).await;
|
|
+ if item_serialization_flushed {
|
|
+ let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
|
|
+ item_ids_by_kind
|
|
+ .into_iter()
|
|
+ .map(|(item_kind, loaded_items)| {
|
|
+ SerializableItemRegistry::cleanup(
|
|
+ item_kind,
|
|
+ serialized_workspace.id,
|
|
+ loaded_items,
|
|
+ window,
|
|
+ cx,
|
|
+ )
|
|
+ .log_err()
|
|
+ })
|
|
+ .collect::<Vec<_>>()
|
|
+ })?;
|
|
+
|
|
+ futures::future::join_all(clean_up_tasks).await;
|
|
+ }
|
|
|
|
workspace
|
|
.update_in(cx, |workspace, window, cx| {
|
|
@@ -9001,6 +9093,7 @@ fn deserialize_remote_project(
|
|
} else {
|
|
persistence::DB.next_id().await?
|
|
};
|
|
+ persistence::DB.ensure_workspace_row(workspace_id).await?;
|
|
|
|
Ok((workspace_id, serialized_workspace))
|
|
})
|