Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
## [Unreleased]

### Added
- [#94] Client/cluster introspection: `FoundationDB::getClientVersion()`
(backed by `fdb_get_client_version`, the version/build of the loaded
`libfdb_c`), `Database::getServerProtocol()` (backed by
`fdb_database_get_server_protocol` through the new
`CrazyGoat\FoundationDB\Future\FutureUInt64` future type and the newly
bound `fdb_future_get_uint64` accessor), and
`FoundationDB::onNetworkThreadCompletion(callable)` for flushing
traces/metrics at shutdown. For safety the completion hook is invoked on
the PHP main thread from `NativeClient::stopNetwork()` after the network
thread has been joined — PHP is never executed on the FDB network thread.
Unit tests in `tests/Unit/FutureUInt64Test.php` and
`tests/Unit/NativeClientCompletionHookTest.php`; integration tests in
`tests/Integration/DatabaseMonitoringTest.php` and
`tests/Integration/NetworkLifecycleTest.php`; `docs/advanced.md` updated.
- [#91] `ReadTransaction::getMappedRange()` — single-round-trip index lookups
backed by `fdb_transaction_get_mapped_range` and
`fdb_future_get_mappedkeyvalue_array` (the latter through the new
Expand Down
16 changes: 16 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ $status = json_decode($statusJson, true);

// Or get the parsed array directly (consistent with AdminClient::getClusterStatus())
$status = $db->getClientStatus(asArray: true);

// Client library version of the loaded libfdb_c (useful in bug reports
// and in multi-version-client setups)
echo FoundationDB::getClientVersion();

// Protocol version spoken by the cluster — lets you detect whether the
// loaded client library can talk to it
echo $db->getServerProtocol();

// Run a callable once when the FDB network thread stops at process shutdown
// (useful for flushing traces/metrics). The callable runs on the PHP main
// thread, after the network thread has been joined — never on the network
// thread itself.
FoundationDB::onNetworkThreadCompletion(function (): void {
// flush metrics/traces here
});
```

## Connection Strings
Expand Down
19 changes: 19 additions & 0 deletions src/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,25 @@ public function getMainThreadBusyness(): float
return $busyness;
}

/**
* Get the protocol version spoken by the cluster this database is
* connected to (backed by `fdb_database_get_server_protocol`).
*
* Lets a client detect the cluster's protocol version and therefore
* whether the loaded client library can talk to it. Called with
* `expected_version = 0`, meaning "no expectation" — the future resolves
* to the protocol version currently in use by the cluster.
*/
public function getServerProtocol(): int
{
$future = new Future\FutureUInt64(
$this->client->fdb->fdb_database_get_server_protocol($this->dpointer, 0),
$this->client,
);

return $future->await();
}

/**
* Get the client status of the database.
*
Expand Down
29 changes: 29 additions & 0 deletions src/FoundationDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,35 @@ public static function getMaxApiVersion(): int
return NativeClient::getInstance()->fdb->fdb_get_max_api_version();
}

/**
* Get the version/build string of the loaded `libfdb_c` client library
* (backed by `fdb_get_client_version`). This is what you want to include
* in bug reports and to disambiguate multi-version-client setups.
*/
public static function getClientVersion(): string
{
// PHP FFI converts `const char*` return values to strings natively.
$version = NativeClient::getInstance()->fdb->fdb_get_client_version();
\assert(\is_string($version));

return $version;
}

/**
* Register a callable to be invoked once when the FDB network thread
* stops (i.e. when the network is stopped at process shutdown or via
* an explicit `NativeClient::stopNetwork()` call). Useful for flushing
* traces/metrics at shutdown.
*
* NOTE: the callable runs on the PHP main thread, strictly after the
* FDB network thread has been joined — PHP must never execute on the
* network thread itself. See `NativeClient::onNetworkThreadCompletion()`.
*/
public static function onNetworkThreadCompletion(callable $hook): void
{
NativeClient::getInstance()->onNetworkThreadCompletion($hook);
}

/**
* Configure the default per-transaction retry-attempt ceiling used
* by `Database::transact()`, `Database::readTransact()`,
Expand Down
40 changes: 40 additions & 0 deletions src/Future/FutureUInt64.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace CrazyGoat\FoundationDB\Future;

use FFI;

/**
* Future resolving through `fdb_future_get_uint64`.
*
* Values are returned as PHP ints. A uint64 result larger than
* `PHP_INT_MAX` cannot be represented and will be reported by PHP FFI as a
* wrapped negative value; the APIs currently backed by this future
* (`Database::getServerProtocol()`) never produce such values.
*/
final class FutureUInt64 extends Future
{
private int $cachedResult = 0;

public function await(): int
{
if ($this->resolved) {
return $this->cachedResult;
}

$this->blockUntilReady();

$out = $this->client->fdb->new('uint64_t');
$this->client->checkError(
$this->client->fdb->fdb_future_get_uint64($this->fpointer, FFI::addr($out)),
);

$this->cachedResult = $out->cdata;
$this->releaseMemory();
$this->resolved = true;

return $this->cachedResult;
}
}
55 changes: 55 additions & 0 deletions src/NativeClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ final class NativeClient
fdb_error_t fdb_setup_network();
fdb_error_t fdb_run_network();
fdb_error_t fdb_stop_network();
const char* fdb_get_client_version();

void fdb_future_destroy(FDBFuture* f);
void fdb_future_release_memory(FDBFuture* f);
Expand All @@ -102,6 +103,7 @@ final class NativeClient
fdb_bool_t fdb_future_is_ready(FDBFuture* f);
fdb_error_t fdb_future_get_error(FDBFuture* f);
fdb_error_t fdb_future_get_int64(FDBFuture* f, int64_t* out);
fdb_error_t fdb_future_get_uint64(FDBFuture* f, uint64_t* out);
fdb_error_t fdb_future_get_double(FDBFuture* f, double* out);
fdb_error_t fdb_future_get_bool(FDBFuture* f, fdb_bool_t* out);
fdb_error_t fdb_future_get_key(FDBFuture* f, const char** out_key, int* out_key_length);
Expand All @@ -121,6 +123,7 @@ final class NativeClient
void fdb_database_destroy(FDBDatabase* d);
double fdb_database_get_main_thread_busyness(FDBDatabase* d);
FDBFuture* fdb_database_get_client_status(FDBDatabase* d);
FDBFuture* fdb_database_get_server_protocol(FDBDatabase* d, uint64_t expected_version);
fdb_error_t fdb_database_set_option(FDBDatabase* d, int option, const void* value, int value_length);
fdb_error_t fdb_database_create_transaction(FDBDatabase* d, FDBTransaction** out_transaction);
FDBFuture* fdb_database_reboot_worker(
Expand Down Expand Up @@ -260,6 +263,21 @@ final class NativeClient
/** @var \FFI\CData|null Handle returned by dlopen('libfdb_c.so'), closed in stopNetwork(). */
private ?CData $fdbLibraryHandle = null;

/**
* Callables registered via `FoundationDB::onNetworkThreadCompletion()`,
* invoked once from stopNetwork() after the FDB network thread has been
* joined. Deliberately NOT registered through the native
* `fdb_add_network_thread_completion_hook()` API: native completion
* hooks run on the FDB network thread, where executing PHP is unsafe
* (the Zend engine is not re-entrant). Running them on the main thread
* after pthread_join() preserves the ordering guarantee (the network
* thread — and therefore all native hooks — has already finished), so
* they are safe places to flush traces/metrics at shutdown.
*
* @var list<callable(): void>
*/
private array $networkCompletionHooks = [];

private function __construct()
{
$this->fdb = FFI::cdef(self::FDB_HEADER, 'libfdb_c.so');
Expand Down Expand Up @@ -386,6 +404,34 @@ private function rollbackNetworkSetup(): void
$this->networkThread = null;
}

/**
* Register a callable to be invoked once when the FDB network thread
* stops, i.e. from stopNetwork() after the network thread has been
* joined. Useful for flushing traces/metrics at shutdown.
*
* NOTE: the callable is executed on the PHP main thread, not on the FDB
* network thread. The native `fdb_add_network_thread_completion_hook()`
* API is intentionally not used for PHP callables: its hook runs on the
* network thread, where executing PHP is unsafe. The deferred invocation
* in stopNetwork() happens strictly after the network thread (and any
* native hooks) has finished, so the ordering guarantee users rely on
* is preserved.
*/
public function onNetworkThreadCompletion(callable $hook): void
{
$this->networkCompletionHooks[] = $hook;
}

/**
* @internal
*
* @return list<callable(): void>
*/
public function getNetworkCompletionHooks(): array
{
return $this->networkCompletionHooks;
}

public function stopNetwork(): void
{
if (!$this->networkStarted) {
Expand Down Expand Up @@ -413,6 +459,15 @@ public function stopNetwork(): void
$this->networkStarted = false;
$this->networkSetup = false;
$this->networkThread = null;

// Invoke registered completion hooks after the network thread has
// been joined and all native state has been torn down, so hooks can
// safely flush traces/metrics. Registered hooks are consumed.
$hooks = $this->networkCompletionHooks;
$this->networkCompletionHooks = [];
foreach ($hooks as $hook) {
$hook();
}
}

public function isNetworkStarted(): bool
Expand Down
17 changes: 17 additions & 0 deletions tests/Integration/DatabaseMonitoringTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace CrazyGoat\FoundationDB\Tests\Integration;

use CrazyGoat\FoundationDB\FoundationDB;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -36,4 +37,20 @@ public function getClientStatusCanReturnParsedArray(): void

self::assertNotEmpty($status);
}

#[Test]
public function getClientVersionReturnsNonEmptyString(): void
{
$version = FoundationDB::getClientVersion();

self::assertNotSame('', trim($version));
}

#[Test]
public function getServerProtocolReturnsPositiveProtocolVersion(): void
{
// FDB protocol versions are large positive integers (e.g. 0x00F08044
// family for 7.0+), so a successful resolution is non-zero.
self::assertGreaterThan(0, $this->getDatabase()->getServerProtocol());
}
}
24 changes: 24 additions & 0 deletions tests/Integration/NetworkLifecycleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,28 @@ public function networkCanBeStoppedAndRestarted(): void
self::assertFalse($client->isNetworkStarted());
self::assertFalse($client->isNetworkSetup());
}

/**
* Runs in a separate process for the same reason as above: stopping the
* network is only possible once per process. Verifies that completion
* hooks registered via `FoundationDB::onNetworkThreadCompletion()` are
* invoked exactly once, after the network has actually stopped.
*/
#[Test]
#[RunInSeparateProcess]
public function networkCompletionHooksRunOnStopNetwork(): void
{
$calls = [];
FoundationDB::onNetworkThreadCompletion(function () use (&$calls): void {
$calls[] = 'flush-traces';
});

FoundationDB::open();
self::assertSame([], $calls, 'Hooks must not run before the network stops');

$client = NativeClient::getInstance();
$client->stopNetwork();

self::assertSame(['flush-traces'], $calls);
}
}
Loading
Loading