lean4-htt/src/Lean/Server/ServerTask.lean
2026-02-05 09:10:32 +00:00

149 lines
5.8 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Marc Huisinga
-/
module
prelude
public import Init.Task
public import Init.System.IO
public section
/-!
This file provides a thin `ServerTask` wrapper over the `Task` API.
All calls to the `Task` API in the language server should go through this API.
The reason for this API is that the elaborator consuming threads from the thread pool should
never hinder language server operations. Specifically, we want to ensure the following:
- All new tasks spawned in the language server must be dedicated so that they cannot be starved
by the elaborator.
- Dedicated tasks are costly, so avoid spawning new tasks for operations that are so cheap that
they can just be done on the current thread instead.
- When mapping or binding a task:
- If the function being mapped is sufficiently cheap that it can run on the current thread,
map it with `sync := true`. This runs the function on the current thread if the task has already
finished, or it reuses the thread of the task if the task has not finished.
This ensures that the function to be executed cannot be starved by the elaborator.
- If the function being mapped is not cheap / costly, map it with `prio := .dedicated`.
This spawns a new thread and thus cannot be starved by the elaborator.
Finally, if the function being mapped is costly, but is already being executed in a dedicated task,
it is fine to pretend that it is a cheap function instead.
In request handlers, the distinction of whether an operation is "cheap" or "costly" should be
decided by the following:
- If the operation is sufficiently fast that it could run on the main task of the language server,
blocking all other communication for a brief moment, then it can be considered cheap.
- If the operation is being executed in a dedicated task that isn't the main task of the server,
it can also be considered cheap.
- Otherwise, it is to be considered costly.
-/
namespace Lean.Server
structure ServerTask (α : Type u) where
task : Task α
deriving Inhabited
instance : Coe (Task α) (ServerTask α) where
coe := .mk
namespace ServerTask
def pure (x : α) : ServerTask α := Task.pure x
def get (t : ServerTask α) : α := t.task.get
def wait (t : ServerTask α) : BaseIO α := IO.wait t.task
def mapCheap (f : α → β) (t : ServerTask α) : ServerTask β :=
t.task.map f (sync := true)
def mapCostly (f : α → β) (t : ServerTask α) : ServerTask β :=
t.task.map f (prio := .dedicated)
def bindCheap (t : ServerTask α) (f : α → ServerTask β) : ServerTask β :=
t.task.bind (f · |>.task) (sync := true)
def bindCostly (t : ServerTask α) (f : α → ServerTask β) : ServerTask β :=
t.task.bind (f · |>.task) (prio := .dedicated)
def join (ts : Array (ServerTask α)) : ServerTask (Array α) := Id.run do
let mut r := ServerTask.pure #[]
for t in ts do
r := r.bindCheap fun acc => t.mapCheap (acc.push ·)
return r
namespace BaseIO
def asTask (act : BaseIO α) : BaseIO (ServerTask α) :=
_root_.BaseIO.asTask (prio := .dedicated) act
def mapTaskCheap (f : α → BaseIO β) (t : ServerTask α) : BaseIO (ServerTask β) :=
BaseIO.mapTask f t.task (sync := true)
def mapTaskCostly (f : α → BaseIO β) (t : ServerTask α) : BaseIO (ServerTask β) :=
BaseIO.mapTask f t.task (prio := .dedicated)
def bindTaskCheap (t : ServerTask α) (f : α → BaseIO (ServerTask β)) : BaseIO (ServerTask β) :=
BaseIO.bindTask t.task (ServerTask.task <$> f ·) (sync := true)
def bindTaskCostly (t : ServerTask α) (f : α → BaseIO (ServerTask β)) : BaseIO (ServerTask β) :=
BaseIO.bindTask t.task (ServerTask.task <$> f ·) (prio := .dedicated)
end BaseIO
namespace EIO
def asTask (act : EIO ε α) : BaseIO (ServerTask (Except ε α)) :=
_root_.EIO.asTask (prio := .dedicated) act
def mapTaskCheap (f : α → EIO ε β) (t : ServerTask α) : BaseIO (ServerTask (Except ε β)) :=
EIO.mapTask f t.task (sync := true)
def mapTaskCostly (f : α → EIO ε β) (t : ServerTask α) : BaseIO (ServerTask (Except ε β)) :=
EIO.mapTask f t.task (prio := .dedicated)
def bindTaskCheap (t : ServerTask α) (f : α → EIO ε (ServerTask (Except ε β))) : BaseIO (ServerTask (Except ε β)) :=
EIO.bindTask t.task (ServerTask.task <$> f ·) (sync := true)
def bindTaskCostly (t : ServerTask α) (f : α → EIO ε (ServerTask (Except ε β))) : BaseIO (ServerTask (Except ε β)) :=
EIO.bindTask t.task (ServerTask.task <$> f ·) (prio := .dedicated)
end EIO
namespace IO
def asTask (act : IO α) : BaseIO (ServerTask (Except IO.Error α)) :=
_root_.IO.asTask (prio := .dedicated) act
def mapTaskCheap (f : α → IO β) (t : ServerTask α) : BaseIO (ServerTask (Except IO.Error β)) :=
IO.mapTask f t.task (sync := true)
def mapTaskCostly (f : α → IO β) (t : ServerTask α) : BaseIO (ServerTask (Except IO.Error β)) :=
IO.mapTask f t.task (prio := .dedicated)
def bindTaskCheap (t : ServerTask α) (f : α → IO (ServerTask (Except IO.Error β))) : BaseIO (ServerTask (Except IO.Error β)) :=
IO.bindTask t.task (ServerTask.task <$> f ·) (sync := true)
def bindTaskCostly (t : ServerTask α) (f : α → IO (ServerTask (Except IO.Error β))) : BaseIO (ServerTask (Except IO.Error β)) :=
IO.bindTask t.task (ServerTask.task <$> f ·) (prio := .dedicated)
end IO
def hasFinished (t : ServerTask α) : BaseIO Bool :=
IO.hasFinished t.task
def waitAny (tasks : List (ServerTask α))
(h : tasks.length > 0 := by exact Nat.zero_lt_succ _) : BaseIO α :=
let ⟨tasks, h⟩ : { tasks : List (Task α) // tasks.length > 0 } :=
⟨tasks.map (·.task), by simpa⟩
IO.waitAny tasks h
def cancel (t : ServerTask α) : BaseIO Unit :=
IO.cancel t.task
end Lean.Server.ServerTask
def Task.asServerTask (t : Task α) : Lean.Server.ServerTask α := .mk t