Skip to content

Commit 894dcad

Browse files
Add README.md
1 parent c93e622 commit 894dcad

2 files changed

Lines changed: 286 additions & 3 deletions

File tree

README.md

Lines changed: 276 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,279 @@
66

77
# Deepslate
88

9-
Deepslate is a durable execution interpreter for a minimal subset of Go and Python.
9+
Write crash resistant programs for financial applications, microservice orchestration, human in the loop applications and agentic workflows.
10+
11+
Deepslate is a durable execution interpreter for a minimal subset of Go and Python, it is an alternative to [temporal](https://temporal.io/) and [azure durable functions](https://learn.microsoft.com/en-us/azure/durable-task/durable-functions/durable-functions-overview). What makes deepslate unique is that unlike existing durable execution engines, deepslate **does not use event replay**. It compiles the flow functions into custom bytecode and runs them on a custom control flow interpreter, deepslate then uses **snapshots** of the interpreter for crash recovery. This enables deepslate replay long running workflows very quickly and use less storage space.
12+
13+
This project is still being worked on and is not ready for public release.
14+
15+
## Getting started
16+
17+
- Create an empty project folder
18+
19+
```
20+
mkdir counter-example
21+
cd counter-example
22+
```
23+
24+
- Create virtual env
25+
26+
```
27+
python3 -m venv .venv
28+
source .venv/bin/activate
29+
```
30+
31+
- Clone deepslate
32+
33+
```
34+
git clone git@github.com:RainingComputers/deepslate.git ../deepslate
35+
```
36+
37+
- Build and install deepslate
38+
39+
```
40+
make -C ../deepslate pypiwheel
41+
pip install ../deepslate/dist/*.whl
42+
```
43+
44+
- Install fastapi and uvicorn
45+
46+
```
47+
pip install fastapi uvicorn
48+
```
49+
50+
- Start postgres using docker
51+
52+
```
53+
docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:18-alpine
54+
```
55+
56+
- Save the example flow and server as `server.py`
57+
58+
```python
59+
import uvicorn
60+
from fastapi import FastAPI
61+
62+
from deepslate import flow, sleep, new_postgres, spawn_workers, spawn_inspector
63+
64+
DBURL = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
65+
66+
67+
# durable flow that counts up forever, one tick per second, can survive crashes and restarts
68+
@flow
69+
async def counter():
70+
count: int = 0
71+
72+
while True:
73+
sleep(1.0)
74+
count = count + 1
75+
76+
def create_app() -> FastAPI:
77+
# connect to postgres and start deepslate
78+
platform, _ = new_postgres(1, DBURL, "localhost:8765", 100, 100, 5)
79+
spawn_fn, _, _, _ = spawn_workers(4, platform, [counter()])
80+
spawn_inspector(platform, "localhost:8080")
81+
82+
app = FastAPI()
83+
app.state.spawn = spawn_fn
84+
85+
# spawn a new durable counter flow with the given name
86+
@app.post("/counter/{name}")
87+
def spawn_counter(name: str):
88+
id_, exists = app.state.spawn(name, "counter", {})
89+
return {"id": id_, "exists": exists}
90+
91+
return app
92+
93+
94+
app = create_app()
95+
96+
97+
if __name__ == "__main__":
98+
uvicorn.run(app, host="0.0.0.0", port=8000, workers=1)
99+
```
100+
101+
- Run the example server
102+
103+
```
104+
python server.py
105+
```
106+
107+
- Spawn a flow
108+
109+
```
110+
curl -X POST http://localhost:8000/counter/my-counter
111+
```
112+
113+
- Visit the dashboard at [localhost:8080](http://localhost:8080) and inspect the `my-counter` isolate
114+
115+
- Kill the server with `Ctrl+C` (leave postgres running)
116+
117+
```
118+
^C
119+
```
120+
121+
- Restart the server
122+
123+
```
124+
python server.py
125+
```
126+
127+
- Visit the dashboard again at [localhost:8080](http://localhost:8080) and observe that `count` continues from where it left off (survived the crash)
128+
129+
## Flow definition language
130+
131+
Deepslate lets you write durable workflows in a restricted subset language within `@flow` functions. The `@flow` function is meant to express only critical glue logic that requires durability. Flow functions can call other ordinary python functions where no restrictions exist.
132+
133+
You can think of a `@flow` function as a durable goroutine that can survive application restarts, deepslate provides golang-like concurrency constructs in python, i.e. spawning, channels (queue), wait group, mutex (lock) and select statement.
134+
135+
- **Flow definition**: an `async def` function marked `@flow`, with typed params and an optional return type
136+
137+
```python
138+
@flow
139+
async def add(a: int, b: int) -> int:
140+
return a + b
141+
```
142+
143+
- **Types**: `int`, `bool`, `str`, `float`, `bytes`, `list[T]`, `dict[K, V]`, `@dataclass` types, the result type `T | DSError`, and sync primitives `dpslio.Queue[T]`, `dpslio.Lock`, `dpslio.WaitGroup`
144+
145+
- **Variable declaration**: every variable needs a type annotation, the value is optional, e.g. `count: int = 0` or `result: bytes | DSError`
146+
147+
- **Assignment**: rebind an already declared variable, e.g. `count = count + 1`
148+
149+
- **Operators**: arithmetic `+ - * / %`, comparison `== != < > <= >=`, boolean `and or not`, and unary `- not`
150+
151+
- **Literals**: `int`, `float`, `str`, `True` or `False`, list `[1, 2, 3]`, and dict `{"a": 1}`
152+
153+
- **if / elif / else**: conditional branching, e.g. `if x > 0: ... elif x == 0: ... else: ...`
154+
155+
- **while**: loop while a condition holds, including infinite loops, e.g. `while True: sleep(1.0)`
156+
157+
- **for**: iterate over a list, or use `enumerate` for index and value, e.g. `for i, num in enumerate(numbers): ...`
158+
159+
- **match / case**: match a value against cases, e.g. `match status: case 200: ... case _: ...`
160+
161+
- **return**: return a value or exit the flow, e.g. `return result` or bare `return`
162+
163+
- **isinstance**: narrow a `T | DSError` union, the idiomatic error check
164+
165+
```python
166+
@flow
167+
async def handle():
168+
data: bytes | DSError
169+
data = http_listen("POST", "/numbers")
170+
if not isinstance(data, bytes):
171+
return data
172+
```
173+
174+
- **sleep**: sleep for given number of seconds, e.g. `sleep(1.0)`, deepslate flows can sleep for days or even months surviving restarts
175+
176+
- **http_listen**: suspend until an HTTP request hits the given route, returning its body, e.g. `data = http_listen("POST", "/numbers")`
177+
178+
- **http_request**: make a durable outbound HTTP, e.g. `resp = http_request("GET", url, {}, b"")`
179+
180+
- **json_marshal and json_unmarshal**: encode to and decode from JSON bytes, decode takes the target type as a generic param, e.g. `nums = json_unmarshal[list[int]](data)`
181+
182+
- **pure function**: `@pure` marked deterministic functions that don't have any side effects, run inline without a checkpoint, e.g. `status = get_status(resp)` where:
183+
184+
```python
185+
@pure
186+
def get_status(resp: HealthResponse) -> str:
187+
return resp.Status
188+
```
189+
190+
- **impure function**: `@impure` marked side-effect functions whose result is run once and checkpointed, e.g. `insert_event(name)` where:
191+
192+
```python
193+
@impure
194+
def insert_event(name: str) -> None:
195+
conn = psycopg2.connect(DBURL)
196+
cur = conn.cursor()
197+
cur.execute("INSERT INTO events (name) VALUES (%s)", (name,))
198+
conn.commit()
199+
conn.close()
200+
```
201+
202+
- **Flow call**: call another `@flow` function directly, it runs durably and returns its value
203+
204+
```python
205+
@flow
206+
async def adder(a: int, b: int) -> int:
207+
return a + b
208+
209+
210+
@flow
211+
async def caller():
212+
total: int = adder(3, 5)
213+
```
214+
215+
- **create_task**: spawn a child flow that runs concurrently
216+
217+
```python
218+
@flow
219+
async def child(n: int):
220+
sleep(1.0)
221+
222+
223+
@flow
224+
async def parent():
225+
dpslio.create_task(child(1))
226+
```
227+
228+
- **Queue**: a durable channel, `put` and `get` are awaited
229+
230+
```python
231+
@flow
232+
async def consumer(ch: dpslio.Queue[int]):
233+
value: int = await ch.get()
234+
235+
236+
@flow
237+
async def producer():
238+
ch: dpslio.Queue[int] = dpslio.Queue[int]()
239+
await ch.put(1)
240+
dpslio.create_task(consumer(ch))
241+
```
242+
243+
- **WaitGroup**: wait for N concurrent children to finish
244+
245+
```python
246+
@flow
247+
async def task(wg: dpslio.WaitGroup):
248+
sleep(1.0)
249+
await wg.done()
250+
251+
252+
@flow
253+
async def parent():
254+
wg: dpslio.WaitGroup = dpslio.WaitGroup()
255+
wg.add(2)
256+
dpslio.create_task(task(wg))
257+
dpslio.create_task(task(wg))
258+
await wg.wait()
259+
```
260+
261+
- **Lock**: a durable mutex for exclusive sections
262+
263+
```python
264+
@flow
265+
async def worker(mu: dpslio.Lock):
266+
await mu.acquire()
267+
count: int = 1
268+
await mu.release()
269+
```
270+
271+
- **select**: proceed with whichever channel is ready first
272+
273+
```python
274+
@flow
275+
async def race(ch1: dpslio.Queue[int], ch2: dpslio.Queue[int]):
276+
result: int = 0
277+
match dpslio.select:
278+
case _ if v := await ch1.get():
279+
result = v
280+
case _ if v := await ch2.get():
281+
result = v
282+
```
283+
284+
- **DSError**: deepslate does not support exceptions, instead the result type `T | DSError` is used, check with `isinstance` and propagate by returning the error

pkg/transpilers/dython/transpiler.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,11 @@ func matchStmt(ctx *transpileContext, node MatchStmtNode, depth int) error {
433433
var caseNodes []CaseClauseNode
434434
for _, caseNode := range node.Cases {
435435
if caseNode.Default {
436-
defaultNode = &BlockNode{Stmts: caseNode.Body, Position: caseNode.Position, Line: caseNode.Line}
436+
defaultNode = &BlockNode{
437+
Stmts: caseNode.Body,
438+
Position: caseNode.Position,
439+
Line: caseNode.Line,
440+
}
437441
} else {
438442
caseNodes = append(caseNodes, caseNode)
439443
}
@@ -474,7 +478,11 @@ func matchStmt(ctx *transpileContext, node MatchStmtNode, depth int) error {
474478

475479
yesBodyStr, err := block(
476480
ctx,
477-
BlockNode{Stmts: caseNode.Body, Position: caseNode.Position, Line: caseNode.Line},
481+
BlockNode{
482+
Stmts: caseNode.Body,
483+
Position: caseNode.Position,
484+
Line: caseNode.Line,
485+
},
478486
depth+i+1,
479487
)
480488
if err != nil {

0 commit comments

Comments
 (0)