tdcmeehan commented on PR #59424:
URL: https://github.com/apache/spark/pull/59424#issuecomment-6173067539
Thanks for moving to the C header, and for documenting the lifecycle so
carefully. I have one concern, about how the header grows. I think it's worth
settling before 4.4.0: version 1 is what library binaries get built against,
and this is the one change that can't come later as an optional function.
**The problem: the header grows by one function per feature.**
1. **Every feature is a new function.** The Compatibility section adds
features as optional functions, and the Limitations section lists six to add
that way: statistics, custom metrics, admission control,
aggregate/sort/partitioning pushdown, cancellation and memory accounting. The
predicate declaration and three-state answer from #59313 are planned the same
way. DSv2 itself keeps growing: master has 37 `Supports*` interfaces, 7 of them
added since July 2025.
2. **Each function is a separate contract that every host and SDK has to
reimplement.** Each has its own argument encoding, ownership, threading and
ordering rules, written in prose. There are five handle types, released by six
different functions. That's where bugs land: @LuciferYang's catch in this PR, a
released data writer passed to `spark_ds_abort_data_writer`, came from one of
those rules, in Spark's own binding.
3. **Signatures are frozen, so a change is a second function.** For example,
giving `spark_ds_read` a task context for memory accounting or cancellation
means a second `read`, and a library that also runs on older releases exports
both.
4. **Support is inferred from symbol names.** It's per library, not per data
source or options, and a library can't declare which predicates it handles. A
missing export, for example a misspelled name, isn't an error: the operation
becomes unsupported, or, for the pushdown and serialize functions, silently
does nothing.
5. **Nothing flows from Spark to the library.** The library reports its
version, but Spark never tells it Spark's version or features, and there's no
callback table. Cancellation, memory accounting and logging all need one.
**Proposal: freeze the C core, and move everything that grows into versioned
messages.**
The header is already halfway there: 14 of its 30 functions pass bytes or
JSON (partitions, state, commit messages, offsets, predicates). This finishes
the move, with one schema. A sketch, with placeholder names:
```c
int32_t spark_ds_abi_version(void);
const char* spark_ds_last_error(void);
/* Opens a data source, reader, writer or data writer. The request says
which, with what it
* needs, plus Spark's protocol version and features. The reply has the
library's version and
* what this object supports. */
int spark_ds_open(const SparkDsHost* host, const uint8_t* request, int64_t
length,
SparkDsSink reply, void* sink, void** handle);
/* Every control operation: schema, pushdown, partitions, offsets, commit,
abort. */
int spark_ds_call(void* handle, const uint8_t* request, int64_t length,
SparkDsSink reply, void* sink);
/* Every data operation, as Arrow C streams: a read fills `out`, a write
consumes `in`. */
int spark_ds_stream(void* handle, const uint8_t* request, int64_t length,
struct ArrowArrayStream* in, struct ArrowArrayStream*
out);
/* Releases any handle. Nothing else does. */
void spark_ds_close(void* handle);
```
- `SparkDsHost` holds Spark's callbacks, such as memory reservation,
cancellation and logging. It starts with its size, and fields are only appended.
- Requests and replies are protobuf messages, in a `.proto` next to the
header. Old code ignores new fields, and field numbers are never reused.
- Spark only sends what the library declared in its open reply, so a new
request never reaches an old library.
- Data stays as it is: zero-copy Arrow C streams.
How the planned additions would land:
| Addition | As optional functions | As messages |
|---|---|---|
| Declared predicates, three-state answer | new pushdown functions | a list
in the open reply, an enum in the pushdown reply |
| Memory accounting, cancellation | a setup function each, or a second
`read` | fields appended to `SparkDsHost` |
| Statistics, partitioning, sort order | a function each | fields in the
planning reply |
| Aggregate and top-N pushdown | a function each | fields in the pushdown
request |
| Admission control | a second `latest_offset` | a field in the offset
request |
| Overwrite by filter | a new function, since `bool overwrite` can't carry a
filter | a write mode and a predicate field |
| Substrait, as @lidavidm suggested | a new pushdown function | another
encoding of the predicate field |
What it buys:
- A feature is new fields, with generated code in every language. No new C
function, binding or ownership rule.
- One ownership rule: request bytes live for the call, the sink copies
replies, `close` releases handles, and Arrow structs follow Arrow's rules.
- Tooling checks compatibility. Spark's CI already runs [`buf
breaking`](https://github.com/apache/spark/blob/4ccf07b6d91e2b126b7001741778ee28f22c6609/.github/workflows/build_and_test.yml#L834-L838)
on the Spark Connect protos, and recorded messages from each release can be
replayed against current libraries.
- A host that runs a library in another process, for example to isolate it
on a shared server, forwards the control messages unchanged.
- If Spark ever wants native data sources on Java 17 and 21, a JNI bridge
would be six functions that never change.
What it costs:
- Library authors need a protobuf runtime. SDKs can vendor it and link it
statically, with hidden symbols, since several libraries can share a process.
Spark already uses protobuf for Spark Connect and the UDF worker protocol.
- The `.proto` and its generated types check the contract, not the C
compiler.
- Control calls pay for encoding. They're rare, and data stays zero-copy.
- Semantics still need specifying. The NaN, null and partial-predicate rules
you wrote carry over unchanged.
Precedents:
- **Arrow** splits it the same way. The C Data Interface is a few frozen
structs. The IPC format is versioned messages: since 1.0, which guarantees
forward and backward compatibility, it has been [extended five
times](https://github.com/apache/arrow/blob/910292405bb82ccbf057b9801f5e4b922ea23788/format/Schema.fbs#L19-L26),
and its [`Feature`
enum](https://github.com/apache/arrow/blob/910292405bb82ccbf057b9801f5e4b922ea23788/format/Schema.fbs#L54-L81)
is "A means of negotiating between a client and server what features a stream
is allowed to use".
- **Comet** shows both sides. Its plan, with 24 operator, 66 expression and
27 aggregate kinds, crosses one JNI function,
[`createPlan`](https://github.com/apache/datafusion-comet/blob/7d8d9b1544b734c85a93cca2cc9d5380ad70accd/spark/src/main/scala/org/apache/comet/Native.scala#L61-L79),
as protobuf, and a [contrib
envelope](https://github.com/apache/datafusion-comet/blob/7d8d9b1544b734c85a93cca2cc9d5380ad70accd/native/proto/src/proto/operator.proto#L49-L81)
means "core's proto never has to change when a contrib is added". Its other
JNI functions went from 9 to 19 in the past year, and `createPlan` gained two
parameters. That's fine for Comet, whose JVM and native halves ship in one jar;
under a public ABI, each would be a second function or a break. **Gluten** also
passes plans as Substrait bytes, and [asks the native
side](https://github.com/apache/gluten/blob/91ae3b22fcb1dbff6937ae517fc0d816c23f4501/gluten-arrow/src/main/java/org/apache/gluten/vectorized/PlanEvaluatorJniWrapper.j
ava#L46-L82) whether it supports a plan.
- **Spark's UDF worker protocol** (SPARK-55278) is protobuf, with
[`Init.protocol_version`](https://github.com/apache/spark/blob/4ccf07b6d91e2b126b7001741778ee28f22c6609/udf/worker/proto/src/main/protobuf/org/apache/spark/udf/worker/udf_message.proto#L162-L167)
and
[`WorkerCapabilities`](https://github.com/apache/spark/blob/4ccf07b6d91e2b126b7001741778ee28f22c6609/udf/worker/proto/src/main/protobuf/org/apache/spark/udf/worker/worker_spec.proto#L113-L135)
that Spark checks during planning.
- **protoc plugins:** the compiler sends its version in the request, and the
plugin answers with
[`supported_features`](https://github.com/protocolbuffers/protobuf/blob/cd68a2eab18e9646bf746182fa3b858c6d9caa7e/src/google/protobuf/compiler/plugin.proto#L79-L115).
- **OpenSSL 3 providers** run in process, with dispatch tables and typed
key/value parameters. Result: "A FIPS provider built from any validated version
may be used together with an OpenSSL library built from any supported release
from OpenSSL 3.0 onwards"
([README-FIPS.md](https://github.com/openssl/openssl/blob/ed57129cd43b77e077c68fd5f7ea951976766f38/README-FIPS.md?plain=1#L36-L41)).
- **ADBC** is a wide C API that works, and shows what it takes. Drivers fill
a function table through a versioned init function, and a driver manager
handles the versions. The table has grown once in four years: revision 1.1.0
(2023) took it from 26 to 55 functions, 21 of them typed option getters and
setters, and needed a
[handshake](https://github.com/apache/arrow-adbc/blob/fe88b72a9917763a1ff2eb694341b5b6da8aa6f4/c/include/arrow-adbc/adbc.h#L1055-L1074)
in which a 1.0.0 driver must fail and be retried. Since then it has added
option keys and info codes, but no functions. DSv2, which this header mirrors,
added 7 optional interfaces in the last 15 months.
- The other way: **DataFusion FFI** mirrors DataFusion's traits as C
structs, and
[recommends](https://github.com/apache/datafusion/blob/0d79252f61bd8777990b2749c216ffd07079c30d/datafusion/ffi/README.md?plain=1#L38-L40)
"the same version of DataFusion by both the producer and consumer", though
it's not strictly required.
Could version 1 take this shape? I'm happy to write the `.proto` for the
current feature set, and the replay test.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]