Skip to content

Commit e5990a0

Browse files
committed
Optimize lock usage and documentation, reduce the risk of deadlocks, and improve file handling logic.
1 parent 532c038 commit e5990a0

File tree

5 files changed

+167
-43
lines changed

5 files changed

+167
-43
lines changed

crates/emmylua_ls/src/context/mod.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,81 @@ pub use workspace_manager::*;
2323

2424
use crate::context::snapshot::ServerContextInner;
2525

26+
// ============================================================================
27+
// LOCK ORDERING GUIDELINES (CRITICAL - Must Follow to Avoid Deadlocks)
28+
// ============================================================================
29+
//
30+
// This module uses multiple locks (RwLock and Mutex) for concurrent access to shared state.
31+
// To prevent deadlocks, **ALL code must acquire locks in the following order**:
32+
//
33+
// ## Global Lock Order (Low to High Priority):
34+
// 1. **diagnostic_tokens** (Mutex) - File diagnostic task tokens
35+
// 2. **workspace_diagnostic_token** (Mutex) - Workspace diagnostic task token
36+
// 3. **update_token** (Mutex) - Reindex/config update token
37+
// 4. **analysis** (RwLock - READ) - Read-only access to EmmyLuaAnalysis
38+
// 5. **workspace_manager** (RwLock - READ) - Read-only access to WorkspaceManager
39+
// 6. **workspace_manager** (RwLock - WRITE) - Exclusive access to WorkspaceManager
40+
// 7. **analysis** (RwLock - WRITE) - Exclusive access to EmmyLuaAnalysis
41+
//
42+
// ## Lock Ordering Rules:
43+
// - **NEVER acquire a lower-priority lock while holding a higher-priority lock**
44+
// - **ALWAYS release locks in reverse order (LIFO) or use explicit scope blocks**
45+
// - **NEVER upgrade a read lock to a write lock (release read, then acquire write)**
46+
// - **Minimize lock scope**: only hold locks for the minimum necessary time
47+
// - **Avoid holding locks across `.await` points when possible**
48+
// - **NEVER call async methods that might acquire locks while holding a lock**
49+
//
50+
// ## Examples:
51+
//
52+
// ### ✅ CORRECT - Proper lock ordering:
53+
// ```rust
54+
// // Acquire workspace_manager read lock first, then release before analysis write
55+
// let should_process = {
56+
// let workspace_manager = context.workspace_manager().read().await;
57+
// workspace_manager.is_workspace_file(&uri)
58+
// };
59+
// if should_process {
60+
// let mut analysis = context.analysis().write().await;
61+
// analysis.update_file(&uri, text);
62+
// }
63+
// ```
64+
//
65+
// ### ❌ WRONG - ABBA deadlock risk:
66+
// ```rust
67+
// let mut analysis = context.analysis().write().await; // Lock A
68+
// // ... operations ...
69+
// let workspace = context.workspace_manager().write().await; // Lock B (while holding A!)
70+
// // DEADLOCK RISK: Another thread might hold B and wait for A
71+
// ```
72+
//
73+
// ### ✅ CORRECT - Release before calling async methods:
74+
// ```rust
75+
// let data = {
76+
// let workspace = context.workspace_manager().read().await;
77+
// workspace.get_config().clone() // Clone data
78+
// }; // Lock released
79+
// init_analysis(data).await; // Safe to call async method
80+
// ```
81+
//
82+
// ### ❌ WRONG - Holding lock while calling async method:
83+
// ```rust
84+
// let workspace = context.workspace_manager().write().await;
85+
// workspace.reload_workspace().await; // May acquire analysis lock internally!
86+
// ```
87+
//
88+
// ## Atomic Operations (Lock-Free):
89+
// The following atomics can be accessed without lock ordering concerns:
90+
// - `workspace_initialized` (AtomicBool)
91+
// - `workspace_diagnostic_level` (AtomicU8)
92+
// - `workspace_version` (AtomicI64)
93+
//
94+
// ## Notes:
95+
// - Use `drop(lock_guard)` explicitly to release locks early when needed
96+
// - Use scope blocks `{ ... }` to limit lock lifetime
97+
// - When in doubt, release all locks before performing complex operations
98+
// - If you need to modify this ordering, update this documentation AND review all call sites
99+
// ============================================================================
100+
26101
pub struct ServerContext {
27102
#[allow(unused)]
28103
conn: Connection,

crates/emmylua_ls/src/context/workspace_manager.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,21 @@ impl WorkspaceManager {
192192
return;
193193
}
194194

195-
let mut analysis = analysis.write().await;
196-
197-
// 在重新索引之前清理不存在的文件
198-
analysis.cleanup_nonexistent_files();
195+
// Perform reindex with minimal lock holding time
196+
{
197+
let mut analysis = analysis.write().await;
198+
// 在重新索引之前清理不存在的文件
199+
analysis.cleanup_nonexistent_files();
200+
analysis.reindex();
201+
// Release lock immediately after reindex
202+
}
199203

200-
analysis.reindex();
204+
// Cancel diagnostics and update status without holding analysis lock
201205
file_diagnostic.cancel_workspace_diagnostic().await;
202206
workspace_diagnostic_status
203207
.store(WorkspaceDiagnosticLevel::Fast.to_u8(), Ordering::Release);
204208

209+
// Trigger diagnostics refresh
205210
if lsp_features.supports_workspace_diagnostic() {
206211
client.refresh_workspace_diagnostics();
207212
} else {

crates/emmylua_ls/src/handlers/completion/providers/equality_provider.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use emmylua_code_analysis::{InferGuard, LuaType, infer_table_field_value_should_be};
2-
use emmylua_parser::{
3-
BinaryOperator, LuaAst, LuaAstNode, LuaBlock, LuaLiteralExpr,
4-
};
2+
use emmylua_parser::{BinaryOperator, LuaAst, LuaAstNode, LuaBlock, LuaLiteralExpr};
53

64
use crate::handlers::completion::{
75
completion_builder::CompletionBuilder, providers::function_provider::dispatch_type,

crates/emmylua_ls/src/handlers/configuration/mod.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,36 @@ pub async fn on_did_change_configuration(
1111
let pretty_json = serde_json::to_string_pretty(&params).ok()?;
1212
log::info!("on_did_change_configuration: {}", pretty_json);
1313

14-
let workspace_manager = context.workspace_manager().read().await;
15-
if !workspace_manager.is_workspace_initialized() {
14+
// Check initialization status and get client config
15+
let (is_initialized, client_id, supports_config_request) = {
16+
let workspace_manager = context.workspace_manager().read().await;
17+
let is_initialized = workspace_manager.is_workspace_initialized();
18+
let client_id = workspace_manager.client_config.client_id;
19+
let supports_config_request = context.lsp_features().supports_config_request();
20+
(is_initialized, client_id, supports_config_request)
21+
};
22+
23+
if !is_initialized {
1624
return Some(());
1725
}
1826

19-
let client_id = workspace_manager.client_config.client_id;
2027
if client_id.is_vscode() {
2128
return Some(());
2229
}
2330

24-
drop(workspace_manager);
25-
26-
let supports_config_request = context.lsp_features().supports_config_request();
27-
2831
log::info!("change config client_id: {:?}", client_id);
32+
33+
// Get new config without holding any locks
2934
let new_client_config = get_client_config(&context, client_id, supports_config_request).await;
30-
let mut workspace_manager = context.workspace_manager().write().await;
31-
workspace_manager.client_config = new_client_config;
3235

33-
log::info!("reloading workspace folders");
34-
workspace_manager.reload_workspace().await;
36+
// Update config and reload - acquire write lock only when necessary
37+
{
38+
let mut workspace_manager = context.workspace_manager().write().await;
39+
workspace_manager.client_config = new_client_config;
40+
log::info!("reloading workspace folders");
41+
workspace_manager.reload_workspace().await;
42+
}
43+
3544
Some(())
3645
}
3746

crates/emmylua_ls/src/handlers/text_document/text_document_handler.rs

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,39 @@ pub async fn on_did_open_text_document(
1111
context: ServerContextSnapshot,
1212
params: DidOpenTextDocumentParams,
1313
) -> Option<()> {
14-
let mut analysis = context.analysis().write().await;
1514
let uri = params.text_document.uri;
1615
let text = params.text_document.text;
17-
let old_file_id = analysis.get_file_id(&uri);
18-
// check is filter file
19-
if old_file_id.is_none() {
20-
let workspace_manager = context.workspace_manager().read().await;
21-
if !workspace_manager.is_workspace_file(&uri) {
22-
return None;
16+
17+
// Check if file should be filtered before acquiring locks
18+
// Follow lock order: workspace_manager (read) -> analysis (write)
19+
let should_process = {
20+
let analysis = context.analysis().read().await;
21+
let old_file_id = analysis.get_file_id(&uri);
22+
if old_file_id.is_some() {
23+
true
24+
} else {
25+
drop(analysis);
26+
let workspace_manager = context.workspace_manager().read().await;
27+
workspace_manager.is_workspace_file(&uri)
2328
}
29+
};
30+
31+
if !should_process {
32+
return None;
2433
}
2534

26-
let file_id = analysis.update_file_by_uri(&uri, Some(text));
27-
if !context.lsp_features().supports_pull_diagnostic() {
35+
// Update file and get diagnostic settings
36+
let (file_id, supports_pull, interval) = {
37+
let mut analysis = context.analysis().write().await;
38+
let file_id = analysis.update_file_by_uri(&uri, Some(text));
2839
let emmyrc = analysis.get_emmyrc();
2940
let interval = emmyrc.diagnostics.diagnostic_interval.unwrap_or(500);
41+
let supports_pull = context.lsp_features().supports_pull_diagnostic();
42+
(file_id, supports_pull, interval)
43+
};
44+
45+
// Schedule diagnostic task without holding any locks
46+
if !supports_pull {
3047
if let Some(file_id) = file_id {
3148
context
3249
.file_diagnostic()
@@ -35,9 +52,11 @@ pub async fn on_did_open_text_document(
3552
}
3653
}
3754

38-
let mut workspace = context.workspace_manager().write().await;
39-
workspace.current_open_files.insert(uri);
40-
drop(workspace);
55+
// Update open files list
56+
{
57+
let mut workspace = context.workspace_manager().write().await;
58+
workspace.current_open_files.insert(uri);
59+
}
4160

4261
Some(())
4362
}
@@ -76,36 +95,54 @@ pub async fn on_did_change_text_document(
7695
context: ServerContextSnapshot,
7796
params: DidChangeTextDocumentParams,
7897
) -> Option<()> {
79-
let mut analysis = context.analysis().write().await;
8098
let uri = params.text_document.uri;
8199
let text = params.content_changes.first()?.text.clone();
82-
let old_file_id = analysis.get_file_id(&uri);
83-
// check is filter file
84-
if old_file_id.is_none() {
85-
let workspace_manager = context.workspace_manager().read().await;
86-
if !workspace_manager.is_workspace_file(&uri) {
87-
return None;
100+
101+
// Check if file should be filtered before acquiring locks
102+
// Follow lock order: workspace_manager (read) -> analysis (write)
103+
let should_process = {
104+
let analysis = context.analysis().read().await;
105+
let old_file_id = analysis.get_file_id(&uri);
106+
if old_file_id.is_some() {
107+
true
108+
} else {
109+
drop(analysis);
110+
let workspace_manager = context.workspace_manager().read().await;
111+
workspace_manager.is_workspace_file(&uri)
88112
}
113+
};
114+
115+
if !should_process {
116+
return None;
89117
}
90118

91-
let file_id = analysis.update_file_by_uri(&uri, Some(text));
92-
let emmyrc = analysis.get_emmyrc();
119+
// Update file and get settings
120+
let (file_id, emmyrc, supports_pull) = {
121+
let mut analysis = context.analysis().write().await;
122+
let file_id = analysis.update_file_by_uri(&uri, Some(text));
123+
let emmyrc = analysis.get_emmyrc();
124+
let supports_pull = context.lsp_features().supports_pull_diagnostic();
125+
(file_id, emmyrc, supports_pull)
126+
};
127+
93128
let interval = emmyrc.diagnostics.diagnostic_interval.unwrap_or(500);
94-
drop(analysis);
95129

130+
// Handle reindex without holding locks
96131
if emmyrc.workspace.enable_reindex {
97132
let workspace = context.workspace_manager().read().await;
98133
workspace.extend_reindex_delay().await;
99-
drop(workspace);
100134
}
101-
if !context.lsp_features().supports_pull_diagnostic() {
135+
136+
// Schedule diagnostic task
137+
if !supports_pull {
102138
if let Some(file_id) = file_id {
103139
context
104140
.file_diagnostic()
105141
.add_diagnostic_task(file_id, interval)
106142
.await;
107143
}
108144
}
145+
109146
Some(())
110147
}
111148

0 commit comments

Comments
 (0)