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
  -> executor

Còn interactive notebook lại cần một model khác:

cell command
  -> execution context / session
  -> language runtime
  -> SparkSession / SparkContext
  -> result / output / progress
  -> UI

Mộ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-band
Working model

Chauffeur 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 answer
Async request/answer shape

Dự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:

ConcernChauffeur role hiện thấy
Control-plane entrypublic/control-facing server surface
RPC filteringallowlist/denylist quanh execution RPC
Driver lifecyclemonitor/start/stop/restart DriverDaemon
Async responseanswer handlers nhận kết quả từ driver
Streaming/progressheartbeat/partial result path
Logs/healthlog 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 / AddExecutionContext là 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:

  • DriverDaemonRoutingServer nhận request phía driver.
  • Request đi vào DriverCorral.handleRPCRequest.
  • DriverCorral có các method/surface:
    • handleRPCRequest
    • executeCommandImpl
    • cancelCommandImpl
    • notifyChauffeur
    • routeDirectNotebookMessageToRepl
    • writeDbfsCommandResultInternal
  • DriverCorral giữ mapping từ ReplId / execution context tới ReplOuterWrapper / 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 sessions

Nế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, ExecutionId quan 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 path

DriverLocal.execute(...) là abstraction quan trọng: input là command/code/options, output là một CommandResultMessage.

DriverCorral.executeCommandImpl
  -> DriverLocal.execute(code, options, ...)
  -> CommandResultMessage

Vì 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 + CommandResultMessage contract.

DriverLocal as the Language Adapter

Đọc theo hướng platform, DriverLocal là adapter giữa command platform và language runtime cụ thể:

RuntimeAdapterExecution styleKey concern
PythonJupyterDriverLocalprocess riêng, Jupyter/ZMQoutput stream, Py4J/Spark bridge, env/hooks
Scala classicScalaDriverLocalin-process JVM REPLclassloader, compiler, driver safety
Scala isolated/serverlessScalaJupyterDriverLocalprocess/kernel riêngisolation, Jupyter protocol
SQLSQLDriverLocalSpark SQL in driverresult table, query history, authz path
RRDriverLocalprocess 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ó JupyterConnection tương ứng connection file chuẩn với 5 socket + HMAC key.
  • JupyterClient poll các channel kiểu Jupyter:
    • shell
    • control
    • iopub
  • Python side có các hook quanh execution:
    • IPYTHON_HOOK_PRE_RUN_CELL
    • DATABRICKS_SHELL_DO_EXECUTE
    • IPYTHON_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
Python có nhiều kênh JVM khác nhau

Đừ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 process

Spark 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.

Why Scala deserves its own pass

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:

  • %sql trong notebook route qua SQLDriverLocal hay qua language magic từ Python/Scala?
  • SQL result được materialize thành TableResult, DatasetInfo, DbfsMetadataOnlyTableResult như 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ề CommandResultMessage khô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.exit result;
  • 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 / ExitResult

Large result path:

DriverLocal.getResultBuffer
  -> CommandResultMessage
  -> DriverCorral.notifyChauffeur
  -> if too large: writeDbfsCommandResultInternal
  -> DBFS / metadata pointer
  -> Chauffeur.handleAnswerExecuteCommand
  -> control plane / UI

Progress / partial result path:

  • progressReporter
  • GET_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/pointer

This 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, telemetry

Open 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 died

Observed surfaces:

  • cancelCommandImpl
  • handleAnswerCancelCommand
  • handleAnswerCancelFailure
  • CancellationStatus
  • 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?
Failure boundaries

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 channel

This 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 / history

Current 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/logs và 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.CommandOptions
    • CommandResultMessages.CommandResultMessage
    • DriverLocal.execute
    • DriverCorral.executeCommandImpl
    • DriverCorral.notifyChauffeur
    • routeDirectNotebookMessageToRepl
  • Với Python path, đối chiếu JVM JupyterClient event với Python DatabricksShell.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.

On this page