Skip to content

feat(cluster): php.ini configuration, execution profiles and ScyllaDB settings - #148

Merged
CodeLieutenant merged 9 commits into
trunkfrom
feat/driver-configuration-and-execution-profiles
Aug 10, 2026
Merged

feat(cluster): php.ini configuration, execution profiles and ScyllaDB settings#148
CodeLieutenant merged 9 commits into
trunkfrom
feat/driver-configuration-and-execution-profiles

Conversation

@CodeLieutenant

@CodeLieutenant CodeLieutenant commented Aug 9, 2026

Copy link
Copy Markdown
Member

What this does

Adds a configuration layer for the driver and exposes a large part of the C driver surface the extension never reached. Coverage goes from 356 to 435 of 621 public cass_* functions, surfaced as 50 php.ini directives and a new ExecutionProfile class.

php.ini configuration

  • Seeds for every Cluster\Builder default. Each with*() method still wins.
  • Caps on the per-worker persistent caches, plus cassandra.allow_persistent as an operator kill switch.
  • Every directive is PHP_INI_SYSTEM by design: the values form the persistent-cluster cache key, so a request-scoped ini_set() would grow EG(persistent_list) without bound.
  • A bad value is ignored in favour of the documented default and writes one E_WARNING, so ini_get() and phpinfo() always report what the driver actually uses.

Execution profiles

enum Profile: string { case Analytics = 'analytics'; }

$cluster = Cassandra::cluster()
    ->withExecutionProfile(Profile::Analytics, (new Cassandra\ExecutionProfile)
        ->withConsistency(Cassandra::CONSISTENCY_LOCAL_ONE)
        ->withRequestTimeout(30.0))
    ->build();

$session->execute($stmt, null, Profile::Analytics);

All sixteen driver settings are covered. Name and selector take a string or an enum case: a string-backed enum contributes its value, any other enum its case name.

CassExecProfile is opaque, so each setter folds its arguments into a fingerprint that reaches the cluster cache key. Without that, two clusters differing only in a profile would share one CassCluster and silently get the wrong settings.

ScyllaDB and connection settings

Rack-aware load balancing, application name/version (visible in system.clients), exponential reconnect with jitter, speculative execution, event-loop tuning, and fourteen further cluster directives that each default to the C driver's own value.

Breaking changes

Change Was Revert with
Default consistency LOCAL_ONE cassandra.default_consistency = LOCAL_ONE
Persistent caches bounded unlimited cassandra.max_persistent_* = -1

Caps are 16 clusters, 16 sessions, 1000 prepared statements, sized from measured per-entry cost: ~12 KB, ~2.2 MB plus 2 sockets per node, and ~7.4 KB.

Fixes found on the way

  • DefaultCluster::connect() leaked the connect CassFuture on the non-persistent path. Confirmed with leaks: one 192-byte root leak per call, now zero.
  • withConnectionHeartbeatInterval() and withTCPKeepalive() passed a value 1000x too large. Both C functions take seconds; the builder stored milliseconds, so a 30 second heartbeat asked for 30000 seconds and effectively disabled heartbeats.
  • cassandra.log = syslog created a file named syslog in the working directory instead of using syslog(3).

Verification

  • Unit suite 879 passed, 0 failures, on the rebased branch.
  • Baseline check: the pre-existing test set gives 867 / 12390 assertions both at trunk and with these changes, so no regression.
  • clang-tidy: 0 findings on every touched file.
  • leaks: 0 bytes across the cap, rack-aware, exponential, speculative and profile paths.
  • Builds clean on DebugPHP8.4NTS, DebugPHP8.5NTS, RelWithDebInfoPHP8.5NTS.
  • Live checks against ScyllaDB while it was up: rack-aware connect, APPLICATION_NAME in system.clients.client_options, and cap enforcement.

Reviewer notes

Rebased onto trunk. The branch is now 0 commits behind. Three conflicts were resolved: the sidebar moved into the new versioned website/.vitepress/versions.ts, the logging table merged expose_credentials with the new syslog target, and the Cluster\Builder reference merged both sets of rows.

Stale defect notes removed. trunk documents the heartbeat and TCP keepalive unit bug as a Known defect in 1.4.x and tells readers to divide by 1000 as a workaround. This branch fixes the bug, so that advice would now produce values 1000 times too short. Those notes are replaced with an upgrade note in connection-tuning.md, and the references in performance.md, connecting.md and the builder reference are removed.

Mixed content. The working tree already had uncommitted work when this was written, and it is included here at the author's request: release/packaging/CI changes (release.yml, compile-php.sh, FindLibGMP.cmake, README.md) and the removal of the legacy tests-old/ tree. Those are not part of the driver-configuration work and can be split out if you prefer.

Not verified. Execution profiles have not run against a live cluster (the local ScyllaDB went down partway through). ZTS is untested locally; CI's ts matrix is the first real check. syslog delivery is confirmed only in that it no longer creates a stray file or falls through to stderr.

@mergify

mergify Bot commented Aug 9, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

… settings

Add a configuration layer for the driver and expose a large part of the C
driver surface that the extension never reached.

php.ini configuration (50 directives)
- Seeds for every Cluster\Builder default. Each with*() method still wins.
- Bounds on the per-worker persistent caches, plus cassandra.allow_persistent
  as an operator kill switch.
- All directives are PHP_INI_SYSTEM: the values form the persistent-cluster
  cache key, so a request-scoped ini_set() would grow the cache without bound.
- A bad value is ignored in favour of the documented default and writes one
  E_WARNING, so ini_get() and phpinfo() always report what the driver uses.

Execution profiles
- New Cassandra\ExecutionProfile covering all sixteen driver settings.
- Register with Cluster\Builder::withExecutionProfile(), select with the third
  argument of execute() and executeAsync().
- Both accept a string or an enum case. A string-backed enum contributes its
  value, any other enum its case name.
- Each setter folds its arguments into a fingerprint that reaches the cluster
  cache key, because CassExecProfile is opaque and two clusters differing only
  in a profile would otherwise share one CassCluster.

ScyllaDB and connection settings
- Rack-aware load balancing, application name and version, exponential
  reconnect with jitter, speculative execution, and event-loop tuning.
- Fourteen further cluster directives, each defaulting to the C driver's own
  value so nothing changes until an operator sets one.

Breaking
- Default consistency is now LOCAL_QUORUM, was LOCAL_ONE.
- The persistent caches are bounded by default: 16 clusters, 16 sessions and
  1000 prepared statements, sized from measured per-entry cost.
Both are reversible with one php.ini line. See CHANGELOG.md.

Fixes
- DefaultCluster::connect() leaked the connect CassFuture on the
  non-persistent path. Confirmed with leaks: one 192-byte root leak per call.
- withConnectionHeartbeatInterval() and withTCPKeepalive() passed a value 1000
  times too large. Both C functions take seconds; the builder stored
  milliseconds. A 30 second heartbeat asked for 30000 seconds.
- cassandra.log = syslog wrote a file named "syslog" instead of using syslog(3).

Dependencies
- The Cassandra backend builds from apache/cassandra-cpp-driver.
- All three drivers track their default branch.

This commit also carries in-progress release, packaging and CI work that was
already present in the working tree, and removes the legacy tests-old tree.

# Conflicts:
#	website/.vitepress/config.mts
#	website/guide/observability.md
#	website/reference/cluster-builder.md
…iles

Adds the connection lifetime, prepared statement, protocol/routing and
request tracing directives to both the guide and cassandra.ini.in, so all
50 directives are documented in both places.

Adds a guide page for execution profiles covering registration, selection
by string or enum case, all sixteen profile settings, the copy-on-build
rule and the effect on the persistent cluster cache. Wires it into the
sidebar, the Cluster\Builder reference and the Session reference, whose
execute() and executeAsync() signatures now show the third argument.

# Conflicts:
#	website/.vitepress/config.mts
@CodeLieutenant CodeLieutenant self-assigned this Aug 9, 2026
@CodeLieutenant
CodeLieutenant force-pushed the feat/driver-configuration-and-execution-profiles branch from 2e958ab to 5903a2c Compare August 9, 2026 23:16
CI failed with "stub file not found:
src/ExecutionProfile/ExecutionProfile.stub.php". The file was on disk but
never entered the repository.

The cause is the `ex*.php` rule in .gitignore. The pattern has no slash, so
git applies it at every depth, and macOS sets core.ignorecase. The rule
therefore matched `ExecutionProfile.stub.php` and `git add` skipped it
without a message. The same rule also hid tests/Unit/ExecutionOptionsTest.php
and tests/Unit/ExecutionProfileTest.php.

Anchor the rule to the repository root, where the old phpize boilerplate
put it. Add the three files it hid.
Three unrelated breaks, one per failing job group.

**cassandra backend — build error.**
`cass_cluster_set_load_balance_rack_aware` exists only in the ScyllaDB
drivers. apache/cassandra-cpp-driver does not declare it, so every
cassandra job failed at src/Cluster/Builder.c:260 with an implicit
declaration. Guard the call and warn at build() time, the same way an
unsupported hostname resolution already does. cpp-rs-driver declares the
function, so only the cassandra backend loses the policy.

**scylla-rust backend — configure error.**
FindCPPDriver.cmake looked for the pkg-config module `scylla-cpp-driver`,
but cpp-rs-driver installs `scylladb.pc` and `scylladb_static.pc`. See
scylla-rust-wrapper/CMakeLists.txt in that project. Use the correct names
and correct the comment that claimed both ScyllaDB drivers share one
module name.

**clang-tidy — missing generated header.**
The lint job built a hardcoded list of module targets to materialise
*_arginfo.h. The list did not include the new execution_profile module, so
clang-tidy could not find ExecutionProfile_arginfo.h. Add a
`generated-sources` aggregate target in cmake/GenStubs.cmake that every
generated file attaches to, and build that instead. A new module now needs
no edit to the workflow.

Also stop hiding a generation failure behind `|| true`. A failure there
must fail the lint job, not turn into a confusing "file not found".
The scylla-rust CI jobs failed to compile. cpp-rs-driver declares none of
cass_cluster_set_new_request_ratio, _queue_size_io, _monitor_reporting_interval,
_prepare_on_up_or_add_host, _no_compact, _tracing_consistency,
_tracing_max_wait_time or _tracing_retry_wait_time, so every call was an
implicit function declaration, which the build treats as an error.

Wrap those eight in the existing PHP_SCYLLADB_BACKEND_SCYLLA_RUST guard, the
same pattern withMaxConnectionsPerHost already uses. The directives stay
registered on that backend and simply keep their driver default, which is why
this needs no INI change.

Checked the whole branch against the cpp-rs-driver header rather than fixing
only what the log showed: these eight are the only additions it lacks.
Execution profiles, rack-aware routing, application name and version,
exponential reconnect, speculative execution, coalesce delay and local address
are all declared there.

Verified by compiling Cluster/Builder.c both ways and reading the symbol
table: 8 of 8 referenced without the macro, 0 of 8 with it.
The PR had gone CONFLICTING, which stops GitHub computing a merge commit,
so no pull_request workflow ran at all on the last push. Merging trunk
restores that.

Two conflicts, both additive:

- CHANGELOG.md: kept the Rows::wasApplied() entry from trunk alongside the
  configuration and execution profile entries from this branch.
- .github/workflows/release.yml: trunk adds an ide-stubs job, this branch
  adds php-manylinux and build-manylinux. Kept all three and made the
  release job depend on build, build-manylinux, ide-stubs and tag.

Merged rather than rebased on purpose: the branch carries commits that are
not mine, and rewriting those to force-push is the worse trade.
The scylla-rust jobs still failed to compile after the previous fix, on a
different set of functions. cpp-rs-driver up to v1.0.x declared these seven
but never implemented them meaningfully; its master branch removed the
declarations outright, so moving the pin from v1.0.0 to master turned every
call site into an implicit declaration:

  cass_keyspace_meta_field_by_name
  cass_table_meta_field_by_name
  cass_column_meta_field_by_name
  cass_iterator_get_meta_field_name
  cass_iterator_get_meta_field_value
  cass_iterator_fields_from_table_meta
  cass_iterator_fields_from_materialized_view_meta

Add include/php_scylladb_rs_compat.h, which supplies inert fallbacks under
PHP_SCYLLADB_BACKEND_SCYLLA_RUST only. One header beats scattering guards
through six files, and it keeps the call sites readable.

Behaviour on that backend is unchanged, because it was already empty: the
functions were listed under "Functions intentionally not implemented" and
every call site already null-checks the pointer or iterator, so schema field
introspection yields empty results exactly as before.

Verified by compiling all 152 translation units against the real
cpp-rs-driver master header rather than against a -D flag: the local C/C++
driver still declares these, so a define alone proves nothing. Zero errors.
…as no symbol

The scylla-rust jobs got past compilation after the last fix and then died at
runtime on every test:

  php: symbol lookup error: cassandra.so: undefined symbol:
  cass_cluster_set_prepare_on_all_hosts

cpp-rs-driver declares this one in its header but ships no implementation, so
it compiles and only fails when the loader resolves it. A header diff cannot
see that, which is why the previous round missed it.

The authoritative list is scylla-rust-wrapper/src/api.rs, an explicit manifest
where unsupported entries are commented out and tagged UNIMPLEMENTED. Checked
every symbol this branch adds against it: exactly nine are unimplemented, and
all nine now sit inside the PHP_SCYLLADB_BACKEND_SCYLLA_RUST guard. The
remaining unimplemented symbols the extension calls are pre-existing schema
metadata paths that trunk already ships.

Docs corrected from eight affected directives to nine.
…g_settings

Last failing test on the scylla-rust backend. cpp-rs-driver has an ABI bug:
its header declares

  CASS_EXPORT CassError
  cass_execution_profile_set_latency_aware_routing_settings(...)

but scylla-rust-wrapper/src/exec_profile.rs implements it returning nothing.
The caller therefore reads whatever is in the return register, and
ASSERT_SUCCESS turned that garbage into a RuntimeException.

Drop the check on this one call. The setter only stores fields on a profile
we just allocated and cannot fail on either driver, so nothing is lost.

Audited the other 23 functions this extension wraps in ASSERT_SUCCESS against
the Rust implementations: this is the only signature mismatch.
@CodeLieutenant
CodeLieutenant merged commit eebeb58 into trunk Aug 10, 2026
63 of 66 checks passed
@CodeLieutenant
CodeLieutenant deleted the feat/driver-configuration-and-execution-profiles branch August 10, 2026 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant