Skip to content

Commit 810a591

Browse files
create djls-tasks crate for easy background work (#12)
* create djls-tasks create for easy background work * fix notifier
1 parent 1799355 commit 810a591

File tree

7 files changed

+371
-2
lines changed

7 files changed

+371
-2
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ djls = { path = "crates/djls" }
77
djls-ast = { path = "crates/djls-ast" }
88
djls-django = { path = "crates/djls-django" }
99
djls-python = { path = "crates/djls-python" }
10+
djls-worker = { path = "crates/djls-worker" }
1011

1112
anyhow = "1.0.94"
1213
serde = { version = "1.0.215", features = ["derive"] }
1314
serde_json = "1.0.133"
15+
tokio = { version = "1.42.0", features = ["full"] }

crates/djls-worker/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "djls-worker"
3+
version = "0.0.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
anyhow = { workspace = true }
8+
tokio = { workspace = true }
9+
10+
async-trait = "0.1.83"

crates/djls-worker/src/lib.rs

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
use anyhow::Result;
2+
use std::sync::Arc;
3+
use tokio::sync::{mpsc, oneshot};
4+
5+
pub trait Task: Send + 'static {
6+
type Output: Send + 'static;
7+
fn run(&self) -> Result<Self::Output>;
8+
}
9+
10+
struct WorkerInner {
11+
sender: mpsc::Sender<TaskMessage>,
12+
shutdown_sender: Option<oneshot::Sender<()>>,
13+
}
14+
15+
#[derive(Clone)]
16+
pub struct Worker {
17+
inner: Arc<WorkerInner>,
18+
}
19+
20+
enum TaskMessage {
21+
Execute(Box<dyn TaskTrait>),
22+
WithResult(
23+
Box<dyn TaskTrait>,
24+
oneshot::Sender<Result<Box<dyn std::any::Any + Send>>>,
25+
),
26+
}
27+
28+
trait TaskTrait: Send {
29+
fn run_boxed(self: Box<Self>) -> Result<Box<dyn std::any::Any + Send>>;
30+
}
31+
32+
impl<T: Task> TaskTrait for T {
33+
fn run_boxed(self: Box<Self>) -> Result<Box<dyn std::any::Any + Send>> {
34+
self.run()
35+
.map(|output| Box::new(output) as Box<dyn std::any::Any + Send>)
36+
}
37+
}
38+
39+
impl Worker {
40+
pub fn new() -> Self {
41+
let (sender, mut receiver) = mpsc::channel(32);
42+
let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
43+
44+
tokio::spawn(async move {
45+
loop {
46+
tokio::select! {
47+
Some(msg) = receiver.recv() => {
48+
match msg {
49+
TaskMessage::Execute(task) => {
50+
let _ = task.run_boxed();
51+
}
52+
TaskMessage::WithResult(task, sender) => {
53+
let result = task.run_boxed();
54+
let _ = sender.send(result);
55+
}
56+
}
57+
}
58+
_ = &mut shutdown_rx => break,
59+
}
60+
}
61+
});
62+
63+
Self {
64+
inner: Arc::new(WorkerInner {
65+
sender,
66+
shutdown_sender: Some(shutdown_tx),
67+
}),
68+
}
69+
}
70+
71+
pub fn execute<T>(&self, task: T) -> Result<()>
72+
where
73+
T: Task + 'static,
74+
{
75+
self.inner
76+
.sender
77+
.try_send(TaskMessage::Execute(Box::new(task)))
78+
.map_err(|e| anyhow::anyhow!("Failed to execute task: {}", e))
79+
}
80+
81+
pub async fn submit<T>(&self, task: T) -> Result<()>
82+
where
83+
T: Task + 'static,
84+
{
85+
self.inner
86+
.sender
87+
.send(TaskMessage::Execute(Box::new(task)))
88+
.await
89+
.map_err(|e| anyhow::anyhow!("Failed to submit task: {}", e))
90+
}
91+
92+
pub async fn wait_for<T>(&self, task: T) -> Result<T::Output>
93+
where
94+
T: Task + 'static,
95+
{
96+
let (tx, rx) = oneshot::channel();
97+
98+
self.inner
99+
.sender
100+
.send(TaskMessage::WithResult(Box::new(task), tx))
101+
.await
102+
.map_err(|e| anyhow::anyhow!("Failed to send task: {}", e))?;
103+
104+
let result = rx
105+
.await
106+
.map_err(|e| anyhow::anyhow!("Failed to receive result: {}", e))??;
107+
108+
result
109+
.downcast()
110+
.map(|b| *b)
111+
.map_err(|_| anyhow::anyhow!("Failed to downcast result"))
112+
}
113+
}
114+
115+
impl Default for Worker {
116+
fn default() -> Self {
117+
Self::new()
118+
}
119+
}
120+
121+
impl Drop for WorkerInner {
122+
fn drop(&mut self) {
123+
if let Some(sender) = self.shutdown_sender.take() {
124+
sender.send(()).ok();
125+
}
126+
}
127+
}
128+
129+
#[cfg(test)]
130+
mod tests {
131+
use super::*;
132+
use anyhow::anyhow;
133+
use std::time::Duration;
134+
use tokio::time::sleep;
135+
136+
struct TestTask(i32);
137+
138+
impl Task for TestTask {
139+
type Output = i32;
140+
141+
fn run(&self) -> Result<Self::Output> {
142+
Ok(self.0 * 2)
143+
}
144+
}
145+
146+
// Basic functionality tests
147+
#[tokio::test]
148+
async fn test_wait_for() {
149+
let worker = Worker::new();
150+
let result = worker.wait_for(TestTask(21)).await.unwrap();
151+
assert_eq!(result, 42);
152+
}
153+
154+
#[tokio::test]
155+
async fn test_submit() {
156+
let worker = Worker::new();
157+
for i in 0..32 {
158+
assert!(worker.execute(TestTask(i)).is_ok());
159+
}
160+
assert!(worker.execute(TestTask(33)).is_err());
161+
assert!(worker.submit(TestTask(33)).await.is_ok());
162+
sleep(Duration::from_millis(50)).await;
163+
}
164+
165+
#[tokio::test]
166+
async fn test_execute() {
167+
let worker = Worker::new();
168+
assert!(worker.execute(TestTask(21)).is_ok());
169+
sleep(Duration::from_millis(50)).await;
170+
}
171+
172+
// Test channel backpressure
173+
#[tokio::test]
174+
async fn test_channel_backpressure() {
175+
let worker = Worker::new();
176+
177+
// Fill the channel (channel size is 32)
178+
for i in 0..32 {
179+
assert!(worker.execute(TestTask(i)).is_ok());
180+
}
181+
182+
// Next execute should fail
183+
assert!(worker.execute(TestTask(33)).is_err());
184+
185+
// But wait_for should eventually succeed
186+
let result = worker.wait_for(TestTask(33)).await.unwrap();
187+
assert_eq!(result, 66);
188+
}
189+
190+
// Test concurrent tasks
191+
#[tokio::test]
192+
async fn test_concurrent_tasks() {
193+
let worker = Worker::new();
194+
let mut handles = Vec::new();
195+
196+
// Spawn multiple concurrent tasks
197+
for i in 0..10 {
198+
let worker = worker.clone();
199+
let handle = tokio::spawn(async move {
200+
let result = worker.wait_for(TestTask(i)).await.unwrap();
201+
assert_eq!(result, i * 2);
202+
});
203+
handles.push(handle);
204+
}
205+
206+
// Wait for all tasks to complete
207+
for handle in handles {
208+
handle.await.unwrap();
209+
}
210+
}
211+
212+
// Test shutdown behavior
213+
#[tokio::test]
214+
async fn test_shutdown() {
215+
{
216+
let worker = Worker::new();
217+
worker.execute(TestTask(1)).unwrap();
218+
worker.wait_for(TestTask(2)).await.unwrap();
219+
// Worker will be dropped here, triggering shutdown
220+
}
221+
sleep(Duration::from_millis(50)).await;
222+
}
223+
224+
// Test error handling
225+
struct ErrorTask;
226+
227+
impl Task for ErrorTask {
228+
type Output = (); // Unit type for error test
229+
230+
fn run(&self) -> Result<Self::Output> {
231+
Err(anyhow!("Task failed"))
232+
}
233+
}
234+
235+
#[tokio::test]
236+
async fn test_error_handling() {
237+
let worker = Worker::new();
238+
239+
// Test error propagation
240+
assert!(worker.wait_for(ErrorTask).await.is_err());
241+
242+
// Test that worker continues to function after error
243+
let result = worker.wait_for(TestTask(21)).await.unwrap();
244+
assert_eq!(result, 42);
245+
}
246+
247+
#[tokio::test]
248+
async fn test_worker_cloning() {
249+
let worker = Worker::new();
250+
let worker2 = worker.clone();
251+
252+
let (result1, result2) = tokio::join!(
253+
worker.wait_for(TestTask(21)),
254+
worker2.wait_for(TestTask(42))
255+
);
256+
257+
assert_eq!(result1.unwrap(), 42);
258+
assert_eq!(result2.unwrap(), 84);
259+
}
260+
261+
#[tokio::test]
262+
async fn test_multiple_workers() {
263+
let worker = Worker::new();
264+
let mut handles = Vec::new();
265+
266+
for i in 0..10 {
267+
let worker = worker.clone();
268+
let handle = tokio::spawn(async move {
269+
let result = worker.wait_for(TestTask(i)).await.unwrap();
270+
assert_eq!(result, i * 2);
271+
});
272+
handles.push(handle);
273+
}
274+
275+
for handle in handles {
276+
handle.await.unwrap();
277+
}
278+
}
279+
}

crates/djls/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ edition = "2021"
66
[dependencies]
77
djls-django = { workspace = true }
88
djls-python = { workspace = true }
9+
djls-worker = { workspace = true }
910

1011
anyhow = { workspace = true }
1112
serde = { workspace = true }
1213
serde_json = { workspace = true }
14+
tokio = { workspace = true }
1315

14-
tokio = { version = "1.42.0", features = ["full"] }
1516
tower-lsp = { version = "0.20.0", features = ["proposed"] }
1617
lsp-types = "0.97.0"

crates/djls/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod documents;
22
mod notifier;
33
mod server;
4+
mod tasks;
45

56
use crate::notifier::TowerLspNotifier;
67
use crate::server::{DjangoLanguageServer, LspNotification, LspRequest};

crates/djls/src/server.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
use crate::documents::Store;
22
use crate::notifier::Notifier;
3+
use crate::tasks::DebugTask;
34
use anyhow::Result;
45
use djls_django::DjangoProject;
6+
use djls_worker::Worker;
7+
use std::sync::Arc;
8+
use std::time::Duration;
59
use tower_lsp::lsp_types::*;
610

711
const SERVER_NAME: &str = "Django Language Server";
@@ -21,16 +25,20 @@ pub enum LspNotification {
2125

2226
pub struct DjangoLanguageServer {
2327
django: DjangoProject,
24-
notifier: Box<dyn Notifier>,
28+
notifier: Arc<Box<dyn Notifier>>,
2529
documents: Store,
30+
worker: Worker,
2631
}
2732

2833
impl DjangoLanguageServer {
2934
pub fn new(django: DjangoProject, notifier: Box<dyn Notifier>) -> Self {
35+
let notifier = Arc::new(notifier);
36+
3037
Self {
3138
django,
3239
notifier,
3340
documents: Store::new(),
41+
worker: Worker::new(),
3442
}
3543
}
3644

@@ -66,6 +74,36 @@ impl DjangoLanguageServer {
6674
MessageType::INFO,
6775
&format!("Opened document: {}", params.text_document.uri),
6876
)?;
77+
78+
// Execute - still sync
79+
self.worker.execute(DebugTask::new(
80+
"Quick task".to_string(),
81+
Duration::from_millis(100),
82+
self.notifier.clone(),
83+
))?;
84+
85+
// Submit - spawn async task
86+
let worker = self.worker.clone();
87+
let task = DebugTask::new(
88+
"Important task".to_string(),
89+
Duration::from_secs(1),
90+
self.notifier.clone(),
91+
);
92+
tokio::spawn(async move {
93+
let _ = worker.submit(task).await;
94+
});
95+
96+
// Wait for result - spawn async task
97+
let worker = self.worker.clone();
98+
let task = DebugTask::new(
99+
"Task with result".to_string(),
100+
Duration::from_secs(2),
101+
self.notifier.clone(),
102+
);
103+
tokio::spawn(async move {
104+
let _ = worker.wait_for(task).await;
105+
});
106+
69107
Ok(())
70108
}
71109
LspNotification::DidChangeTextDocument(params) => {

0 commit comments

Comments
 (0)