Command Execution Path
Một notebook command đi từ UI/control plane xuống DriverCorral, vào language runtime, rồi trả result/progress/status về platform như thế nào.
Question
Khi mình bấm Run một cell trong Databricks notebook, command đó thật sự đi qua những boundary nào?
Mình muốn lần lại flow end-to-end:
Notebook UI / API
-> control plane / proxy
-> runtime control boundary
-> Chauffeur / DriverDaemon
-> DriverCorral
-> language-specific DriverLocal / kernel / REPL
-> SparkSession / SparkContext / Spark job
-> output/result/progress
-> UI / logs / historyĐiểm cần hiểu không chỉ là “cell chạy ở đâu”, mà là Databricks biến notebook cell thành một command object có context, credential, language state, output streaming, cancellation, error/result serialization và telemetry như thế nào.
Why This Matters
Spark OSS không có khái niệm “notebook command” như một first-class runtime object.
Trong Spark truyền thống, mental model thường là:
user program
-> Spark driver
-> SparkContext
-> job / stage / task
-> executorCòn interactive notebook lại cần một model khác:
cell command
-> execution context / session
-> language runtime
-> SparkSession / SparkContext
-> result / output / progress
-> UIMột notebook runtime không thể spark-submit lại từ đầu cho từng cell. Nó phải giữ runtime sống lâu, nhận nhiều command nhỏ, preserve state giữa các lần chạy, stream output dần về UI, cho phép cancel/interrupt, và nếu một cell fail thì session vẫn sống tiếp.
Vì vậy command path là bridge quan trọng giữa hai phần note hiện tại:
- architecture overview cho thấy các component như
ChauffeurDaemon,DriverDaemon,DriverCorral, Python kernel, Scala REPL; - command path giải thích các component đó phối hợp với nhau như thế nào khi user thật sự chạy một cell.
Reading Frame
Page này đang là raw note, không phải reference doc.
Mục tiêu của mình ở đây là đọc các signal trong runtime snapshot rồi dựng một mental model có thể sửa tiếp. Tên class/method/event chỉ là mốc để bám vào khi suy luận, không phải phần chính của bài. Phần chính vẫn là câu hỏi: một cell trong notebook được Databricks biến thành command như thế nào, command đó sống ở đâu, đi qua boundary nào, và vì sao flow này khác Spark OSS.
Mình sẽ giữ ba mức confidence trong đầu:
- cái đã thấy trực tiếp từ snapshot/log/class surface;
- cái nhiều signal cùng gợi ý nhưng chưa có trace full;
- cái mới là giả thuyết để nối các mảnh lại.
Nhưng prose bên dưới sẽ viết theo kiểu “mình đang hiểu flow là…” thay vì liệt kê tài liệu tham khảo.
Current High-Level Model
Notebook command trong DBR có thể đọc thành 5 chặng lớn:
[1] Control plane -> Chauffeur
[2] Chauffeur -> DriverDaemon / DriverCorral
[3] DriverCorral -> language-specific DriverLocal
[4] DriverLocal -> actual REPL / kernel / SQL / R runtime
[5] CommandResultMessage -> Chauffeur / control plane / UIĐiều quan trọng là flow này không giống một RPC sync duy nhất. Nó giống một command pipeline bất đồng bộ hơn:
request goes down
-> command is queued/dispatched
-> language runtime executes
-> output/progress may stream back while running
-> final result is normalized
-> large result may be written out-of-bandChauffeur là runtime control boundary phía ngoài. DriverDaemon là long-lived JVM runtime service. DriverCorral là dispatcher cho REPL/execution context. DriverLocal là abstraction chuẩn hóa từng language runtime. CommandResultMessage là abstraction chuẩn hóa output/result/error.
Stage 1 — Control Plane to Chauffeur
Câu hỏi đầu tiên: request từ notebook UI/control plane vào runtime bằng cửa nào?
Mình đang đọc Chauffeur như cái cửa runtime mà control plane nói chuyện trước khi đụng vào driver thật.
Nếu chỉ là Spark OSS thì mình dễ tưởng notebook UI gọi thẳng vào driver. Nhưng trong DBR, driver không đứng trần trước notebook/control plane. Ở phía ngoài có Chauffeur: nó expose server surface, có execution handler, có allowlist/denylist cho RPC, có monitor driver, và có cặp request/answer riêng cho command.
Mấy signal như ExecuteCommand, ExecuteBatch, AddExecutionContext, AnswerExecuteCommand làm mình nghĩ command không phải một function call sync kiểu “gửi code xuống rồi trả result ngay”. Nó giống một message đi vào runtime hơn: request xuống Chauffeur, Chauffeur route vào driver side, driver xử lý xong hoặc đang xử lý thì bắn answer/progress ngược lên.
Working interpretation:
Control plane / notebook UI
-> Chauffeur public server
-> execution RPC allowlist / routing
-> sendMessageToDriver(...)Điểm quan trọng: Chauffeur có vẻ không gọi driver theo kiểu sync rồi ngồi chờ response inline. Pattern quan sát được giống async request/answer hơn:
Chauffeur sendMessageToDriver
-> Driver handles request
-> Driver calls back / notifies Chauffeur with answerDựa trên các signal hiện có, request đi xuống qua sendMessageToDriver, còn answer đi ngược lên qua handler kiểu handleAnswerExecuteCommand. Trong lúc command đang chạy, driver có thể gửi heartbeat/progress/partial result thay vì chờ command xong mới trả một response duy nhất.
What Chauffeur Adds
Nếu chỉ nhìn từ Spark OSS, một notebook cell có thể tưởng là “UI gọi thẳng driver”. Nhưng Chauffeur cho thấy Databricks đặt một boundary phía trước driver:
| Concern | Chauffeur role hiện thấy |
|---|---|
| Control-plane entry | public/control-facing server surface |
| RPC filtering | allowlist/denylist quanh execution RPC |
| Driver lifecycle | monitor/start/stop/restart DriverDaemon |
| Async response | answer handlers nhận kết quả từ driver |
| Streaming/progress | heartbeat/partial result path |
| Logs/health | log tail, startup info, chauffeur state |
Điều này làm command path khác với một direct notebook-to-driver model. Driver không phải component duy nhất nhận mọi thứ từ platform; Chauffeur là một runtime boundary riêng.
Open Questions
- UI/control plane luôn đi qua Chauffeur hay có path trực tiếp tới DriverDaemon trong một số case?
- Exact schema của
ExecuteCommand/ExecuteBatch/AddExecutionContextlà gì? - Các thế hệ API Gen 1 / Gen 3 / ReplExec coexist như thế nào trong runtime hiện tại?
Stage 2 — Chauffeur to DriverDaemon / DriverCorral
Sau khi Chauffeur nhận request, request được gửi vào DriverDaemon side.
Observed signals:
DriverDaemonRoutingServernhận request phía driver.- Request đi vào
DriverCorral.handleRPCRequest. DriverCorralcó các method/surface:handleRPCRequestexecuteCommandImplcancelCommandImplnotifyChauffeurrouteDirectNotebookMessageToReplwriteDbfsCommandResultInternal
DriverCorralgiữ mapping từReplId/ execution context tớiReplOuterWrapper/DriverWrapper/DriverLocal.
Mental model:
Chauffeur
-> sendMessageToDriver(...)
-> DriverDaemonRoutingServer
-> DriverCorral.handleRPCRequest
-> getReplWrapper(ReplId / ExecutionContextId)
-> executeCommandImpl(...)DriverCorral là trung tâm của command/session dispatch. Nó không phải Spark driver theo nghĩa OSS; nó là lớp Databricks đặt lên trên long-lived Driver JVM để điều phối nhiều REPL/session.
Repl/session mapping
Observed model từ snapshot:
Chauffeur session / ExecutionContextId
-> StartRepl(ReplId, language, executionContextId)
-> DriverCorral
-> ReplOuterWrapper
-> DriverWrapper
-> DriverLocal
-> shared SparkContext / per-session SparkSession hooksĐiểm này nối trực tiếp với evidence ở page Interactive Runtime: nhiều notebook có thể share Spark application/SparkContext, nhưng mỗi notebook/session vẫn có boundary riêng ở SparkSession/REPL/language runtime.
Why Corral Is the Key Abstraction
Tên DriverCorral khá đúng nghĩa: nó “quây” nhiều REPL/session trong cùng một driver runtime.
one DriverDaemon JVM
-> one shared SparkContext / Spark application boundary
-> many ReplId / execution contexts
-> many DriverLocal instances
-> many notebook/language sessionsNếu không có lớp kiểu DriverCorral, shared compute sẽ bị lẫn giữa hai cực:
- hoặc mỗi notebook cần một driver/SparkContext riêng, rất đắt và không còn interactive shared compute;
- hoặc nhiều notebook chạy thẳng vào một driver process chung mà không có session/command boundary rõ, rất nguy hiểm cho isolation, cancellation, result routing và observability.
Open Questions
ReplId,ExecutionContextId,SessionId,CommandId,ExecutionIdquan hệ chính xác thế nào?shareSparkSessionảnh hưởng gì tới SparkSession boundary giữa notebook/language?- Có bao nhiêu command queue: Chauffeur-side, DriverCorral-side, language runtime-side?
Stage 3 — DriverCorral to DriverLocal
DriverCorral không trực tiếp chạy mọi language. Nó route command tới DriverLocal tương ứng với language/runtime.
Observed class hierarchy hiện tại:
DriverLocal
├── JupyterDriverLocal / PythonDriverLocalBase -> Python ipykernel
├── ScalaDriverLocal -> Scala in-process / DriverILoop
├── ScalaJupyterDriverLocal -> Scala isolated/serverless / Almond-like kernel
├── SQLDriverLocal -> Spark SQL path
└── RDriverLocal -> R process / socket pathDriverLocal.execute(...) là abstraction quan trọng: input là command/code/options, output là một CommandResultMessage.
DriverCorral.executeCommandImpl
-> DriverLocal.execute(code, options, ...)
-> CommandResultMessageVì vậy command path có một điểm thống nhất:
- Databricks có nhiều language runtime khác nhau;
- nhưng phía trên chúng được normalize thành
DriverLocal+CommandResultMessagecontract.
DriverLocal as the Language Adapter
Đọc theo hướng platform, DriverLocal là adapter giữa command platform và language runtime cụ thể:
| Runtime | Adapter | Execution style | Key concern |
|---|---|---|---|
| Python | JupyterDriverLocal | process riêng, Jupyter/ZMQ | output stream, Py4J/Spark bridge, env/hooks |
| Scala classic | ScalaDriverLocal | in-process JVM REPL | classloader, compiler, driver safety |
| Scala isolated/serverless | ScalaJupyterDriverLocal | process/kernel riêng | isolation, Jupyter protocol |
| SQL | SQLDriverLocal | Spark SQL in driver | result table, query history, authz path |
| R | RDriverLocal | process riêng, socket thô | output buffer, process lifecycle |
Abstraction này giải thích vì sao page này nên bắt đầu từ command lifecycle trước, rồi mới tách Python/Scala/R/SQL. Nếu nhảy thẳng vào Python kernel hoặc Scala REPL, dễ miss điểm lớn hơn: Databricks đang normalize nhiều runtime khác nhau thành cùng một command/result contract.
Stage 4 — Language Runtime Paths
Python path — Jupyter wire protocol
Python là case dễ tách boundary hơn vì Python vốn chạy ngoài JVM.
Observed signals:
- Python notebook runtime là ipykernel thật, với Databricks subclasses:
IPKernelApp -> BaseDatabricksKernelApp -> DatabricksKernelApp
IPythonKernel -> LspAwareKernel -> DatabricksKernel
ZMQInteractiveShell -> DatabricksShell
jupyter_client.Session -> DatabricksSession- Driver JVM đóng vai trò Jupyter client.
- Có
JupyterConnectiontương ứng connection file chuẩn với 5 socket + HMAC key. JupyterClientpoll các channel kiểu Jupyter:- shell
- control
- iopub
- Python side có các hook quanh execution:
IPYTHON_HOOK_PRE_RUN_CELLDATABRICKS_SHELL_DO_EXECUTEIPYTHON_HOOK_POST_RUN_CELL
Python command path working model:
DriverCorral
-> JupyterDriverLocal.execute(...)
-> JupyterClient sends execute_request over ZMQ shell channel
-> Python ipykernel / DatabricksShell.do_execute
-> iopub emits stdout/stderr/display/MIME bundle
-> Driver collects reply/output
-> CommandResultMessageĐừng gộp hết thành Py4J. Snapshot gợi ý ít nhất ba kênh khác nhau:
- Jupyter wire protocol: JVM client -> Python kernel, dùng cho command execution/output.
- Py4J gateway: Python client -> JVM server, dùng cho Spark/DBUtils/helper objects.
- Spark Connect path: Python -> JVM qua Spark Connect/UDS ở một số compute mode.
What Runs Where
Driver JVM
-> DriverCorral / JupyterDriverLocal
-> Jupyter client, result collection, Spark bridge hooks
Python process
-> ipykernel / DatabricksKernel
-> DatabricksShell.do_execute
-> Python variables, imports, display hooks
Spark JVM state
-> SparkSession / SparkContext
-> accessed from Python through Py4J or Spark Connect path depending compute modeĐiểm quan trọng: Python variable state nằm trong Python kernel process, còn Spark application/SparkContext nằm trong Driver JVM. Vì vậy cùng một notebook có thể bridge qua SparkSession, nhưng Python namespace vẫn là một runtime boundary riêng.
Scala path — in-process REPL vs isolated kernel
Scala khó hơn Python vì Scala/JVM có thể sống rất gần Driver JVM.
Observed split hiện tại:
ScalaDriverLocal
-> Scala in-process path
-> DriverILoop / SparkILoop style interpreter
ScalaJupyterDriverLocal
-> isolated/serverless path
-> Jupyter protocol / separate kernel processSpark OSS baseline:
- Scala REPL compile code runtime thành class.
- Closure/task trên executor có thể cần class sinh ra từ REPL.
- Driver classpath/class server quan trọng hơn Python case.
Databricks-specific question:
Scala cell
-> compile/evaluate ở đâu?
-> REPL state giữ ở object/session nào?
-> generated classes đi tới executor bằng path nào?
-> làm sao tránh user Scala code phá runtime service/shared Driver JVM?Page này chưa cần giải hết Scala; chỉ cần đặt Scala là một branch cần deep dive riêng sau command path.
Python naturally gives Databricks a process boundary. Scala classic does not. If Scala code runs in-process with the Driver JVM, then classloader, compiler state, generated classes, interrupt/cancel, and security boundary all become sharper problems.
SQL path
SQL có vẻ đi qua SQLDriverLocal và Spark SQL path trong Driver JVM.
Questions:
%sqltrong notebook route qua SQLDriverLocal hay qua language magic từ Python/Scala?- SQL result được materialize thành
TableResult,DatasetInfo,DbfsMetadataOnlyTableResultnhư thế nào? - Query history gắn với command ID / execution context ID ở đâu?
SQL path là chỗ nối page này với các page sau:
04-authz-fgac.mdx: catalog/analyzer/policy check;07-observability-runtime-contract.mdx: query history/result/log correlation;05-credential-storage-boundary.mdx: storage credential and filesystem path.
R path
R không dùng Jupyter protocol theo current reverse docs; có vẻ dùng socket thô và bộ RDriverLocal, RShell, ROutputBuffer.
Questions:
- R process lifecycle giống Python đến mức nào?
- Result/output buffer của R có normalize về
CommandResultMessagekhông? - R path còn relevant trong compute modes nào?
Stage 5 — Result, Output, Progress, and Large Results
Một cell không chỉ trả “return value”. Nó có thể trả nhiều dạng output:
- stdout/stderr;
- ANSI text;
- HTML;
- image;
- MIME bundle;
- table/dataframe result;
- error result;
- input request;
dbutils.notebook.exitresult;- partial progress/result;
- large result stored out-of-band.
Observed abstraction:
CommandResultMessage
├─ CommandResultSuccess / Failure
├─ TextResult / HtmlResult / ImageResult / MIMEBundleResult
├─ TableResult / DatasetInfo / PlotOptions
├─ StreamableResult
├─ IntermediateDbfsResult
├─ DbfsMetadataOnlyTableResult
├─ ErrorResult / IpynbErrorResult / SqlErrorResult
└─ InputRequest / ExitResultLarge result path:
DriverLocal.getResultBuffer
-> CommandResultMessage
-> DriverCorral.notifyChauffeur
-> if too large: writeDbfsCommandResultInternal
-> DBFS / metadata pointer
-> Chauffeur.handleAnswerExecuteCommand
-> control plane / UIProgress / partial result path:
progressReporterGET_FINAL_PROGRESS- heartbeat types such as
ForegroundCommandPartialResultHeartbeat
This is important because it shows command execution is not one synchronous RPC returning one blob. It is closer to a streaming/async command system with separate result paths.
Why Large Results Matter Architecturally
A notebook result can be much larger than a normal RPC response. Databricks therefore needs at least two result modes:
small result
-> inline CommandResultMessage
large result / table result
-> write to DBFS/storage-like backing path
-> return metadata/pointerThis is not just an optimization detail. It means storage/filesystem boundary can appear even in “display result” path, not only in user code reading/writing data.
Pre-command and Post-command Hooks
Before each command, DBR appears to run a setup sequence. This matters because a cell is not just “send text to REPL”. It is closer to a small managed transaction.
Observed event categories include:
PUSH_COMMAND_ONTO_DRIVER_INPUT_QUEUE
TAKE_COMMAND_FROM_DRIVER_INPUT_QUEUE
PRE_COMMAND_...
PYTHON_PRE_COMMAND_...
SEND_COMMAND_TO_REPL
WAIT_FOR_COMMAND_RESULT
POST_COMMAND_...Potential setup work:
- refresh AWS/Azure/Databricks API credentials;
- setup credential passthrough context;
- Spark Connect running-command notification;
- Python FUSE / import hooks / sys.path / libraries;
- Delta/session properties;
- display handlers;
- WSFS flush or cleanup.
The important reading is:
cell command
!= raw text sent to interpreter
cell command
= managed unit with setup, context, execution, result, cleanup, telemetryOpen question:
- Which pre/post hooks are universal, and which are language-specific?
Cancellation and Failure Path
A managed command system must distinguish:
cell failed
!= notebook session died
!= DriverDaemon died
!= SparkContext diedObserved surfaces:
cancelCommandImplhandleAnswerCancelCommandhandleAnswerCancelFailureCancellationStatus- control channel in Jupyter path
- DriverDaemonMonitor / Chauffeur monitor for process-level failures
Questions:
- Cancel routes through DriverCorral first or language runtime first?
- Python interrupt uses Jupyter control channel?
- Scala in-process interrupt uses thread/REPL cancellation?
- How does DBR prevent one failed command from corrupting the whole REPL/session?
Command failure, REPL/session failure, DriverDaemon failure, SparkContext failure, and compute failure are different boundaries. One goal of this series is to keep those boundaries separate instead of calling all of them “driver failed”.
Direct Notebook Channel / DNC
There are signals for a direct notebook channel that can bypass Chauffeur for some message types:
routeDirectNotebookMessageToRepl
-> executeDNCMessageToShell / executeDNCMessageToControl
-> ZMQ shell/control channelThis likely matters for low-latency notebook interactions, but should stay clearly marked as needs verification until the exact product path is known.
Questions:
- Which messages use DNC?
- Is DNC only for notebook interactivity/autocomplete/debug/control, or also execution?
- How does DNC relate to Chauffeur request/answer path?
Summary Model
Control plane / UI
│
▼
ChauffeurDaemon
│ sendMessageToDriver + DriverRpcCache
▼
DriverDaemonRoutingServer
│
▼
DriverCorral
├─ getReplWrapper(ReplId / ExecutionContextId)
├─ executeCommandImpl
├─ cancelCommandImpl
├─ routeDirectNotebookMessageToRepl
└─ notifyChauffeur
│
▼
DriverLocal.execute(...)
├─ ScalaDriverLocal -> DriverILoop / in-process Scala
├─ JupyterDriverLocal -> Python ipykernel over ZMQ
├─ ScalaJupyterDriverLocal -> isolated Scala kernel over ZMQ
├─ SQLDriverLocal -> Spark SQL path
└─ RDriverLocal -> R socket path
│
▼
CommandResultMessage
├─ inline result
├─ partial/progress heartbeat
└─ large result -> DBFS pointer
│
▼
Chauffeur answer handlers
-> control plane / UI / logs / historyCurrent Answer
Databricks biến notebook thành một command platform đặt trên long-lived Spark runtime.
ChauffeurDaemon là runtime control boundary phía ngoài; DriverDaemon host long-lived driver/runtime service; DriverCorral map execution context/REPL và dispatch command; mỗi language được normalize qua DriverLocal; output/result/error/progress được gom về CommandResultMessage hoặc streaming/DBFS-backed result path rồi trả ngược lên platform.
Điểm quan trọng nhất: command path không chỉ là “send code to Spark”. Nó là nơi Databricks gắn session state, credential refresh, language runtime isolation, result serialization, cancellation, progress heartbeat và observability vào từng cell.
Still Missing / Need Verify
- Một trace end-to-end từ log thật cho một command cụ thể.
- Exact schema của
ExecuteCommand,CommandOptions,RunnableCommand,CommandResultMessageở mức field quan trọng. - Correlation giữa
CommandId,ExecutionId,ReplId,ExecutionContextId, query ID và log/audit/result ID. - UI/control plane path nào đi qua Chauffeur, path nào dùng DNC/direct notebook channel.
- Python/Scala/SQL/R path khác nhau đến mức nào ở cancellation, progress và result serialization.
- Large result path ghi DBFS cụ thể ở đâu, có relation gì với workspace/DBFS/UC/storage credential không.
Next Probes
- Tìm một command ID trong
chauffeur/logsvà driver logs, rồi map request → answer → result/progress. - Inspect event names around
ExecuteCommandEvent$EventTypeđể dựng timeline chính xác hơn. - Inspect class surfaces:
ExecutionMessages.CommandOptionsCommandResultMessages.CommandResultMessageDriverLocal.executeDriverCorral.executeCommandImplDriverCorral.notifyChauffeurrouteDirectNotebookMessageToRepl
- Với Python path, đối chiếu JVM
JupyterClientevent với PythonDatabricksShell.do_execute/ IPython hooks. - Với Scala path, đọc thêm
DriverILoop/ScalaDriverLocalđể xác định REPL state và class generation boundary.
How This Page Connects to Next Deep Dives
- Runtime Lifecycle and Session Boundaries: boot/readiness/restart/lifecycle and the shared-runtime versus per-notebook boundary; not command details.
03-identity-and-run-as.mdx: identity/principal should attach to command context somewhere in this flow.04-authz-fgac.mdx: SQL command path should hit catalog/analyzer/policy checks.05-credential-storage-boundary.mdx: pre-command credential setup and large result/storage paths connect command to filesystem boundary.07-observability-runtime-contract.mdx: command ID / query ID / result ID / logs / audit correlation should be built on top of this flow.