-
Notifications
You must be signed in to change notification settings - Fork 263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PHPLIB-476: Consider transaction readPreference in select_server #1178
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -544,11 +544,11 @@ function with_transaction(Session $session, callable $callback, array $transacti | |
*/ | ||
function extract_session_from_options(array $options): ?Session | ||
{ | ||
if (! isset($options['session']) || ! $options['session'] instanceof Session) { | ||
return null; | ||
if (isset($options['session']) && $options['session'] instanceof Session) { | ||
return $options['session']; | ||
} | ||
|
||
return $options['session']; | ||
return null; | ||
} | ||
|
||
/** | ||
|
@@ -558,33 +558,43 @@ function extract_session_from_options(array $options): ?Session | |
*/ | ||
function extract_read_preference_from_options(array $options): ?ReadPreference | ||
{ | ||
if (! isset($options['readPreference']) || ! $options['readPreference'] instanceof ReadPreference) { | ||
return null; | ||
if (isset($options['readPreference']) && $options['readPreference'] instanceof ReadPreference) { | ||
return $options['readPreference']; | ||
} | ||
|
||
return $options['readPreference']; | ||
return null; | ||
} | ||
|
||
/** | ||
* Performs server selection, respecting the readPreference and session options | ||
* (if given) | ||
* Performs server selection, respecting the readPreference and session options. | ||
* | ||
* The pinned server for an active transaction takes priority, followed by an | ||
* operation-level read preference, followed by an active transaction's read | ||
* preference, followed by a primary read preference. | ||
* | ||
* @internal | ||
*/ | ||
function select_server(Manager $manager, array $options): Server | ||
{ | ||
$session = extract_session_from_options($options); | ||
$server = $session instanceof Session ? $session->getServer() : null; | ||
|
||
// Pinned server for an active transaction takes priority | ||
if ($server !== null) { | ||
return $server; | ||
} | ||
|
||
// Operation read preference takes priority | ||
$readPreference = extract_read_preference_from_options($options); | ||
if (! $readPreference instanceof ReadPreference) { | ||
// TODO: PHPLIB-476: Read transaction read preference once PHPC-1439 is implemented | ||
$readPreference = new ReadPreference(ReadPreference::PRIMARY); | ||
|
||
// Read preference for an active transaction takes priority | ||
if ($readPreference === null && $session instanceof Session && $session->isInTransaction()) { | ||
/* Session::getTransactionOptions() should always return an array if the | ||
* session is in a transaction, but we can be defensive. */ | ||
$readPreference = extract_read_preference_from_options($session->getTransactionOptions() ?? []); | ||
} | ||
|
||
// Manager::selectServer() defaults to a primary read preference | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since 1.11 (PHPC-1937). |
||
return $manager->selectServer($readPreference); | ||
} | ||
|
||
|
@@ -601,7 +611,11 @@ function select_server_for_aggregate_write_stage(Manager $manager, array &$optio | |
$readPreference = extract_read_preference_from_options($options); | ||
|
||
/* If there is either no read preference or a primary read preference, there | ||
* is no special server selection logic to apply. */ | ||
* is no special server selection logic to apply. | ||
* | ||
* Note: an alternative read preference could still be inherited from an | ||
* active transaction's options, but we can rely on libmongoc to raise a | ||
* "read preference in a transaction must be primary" error if necessary. */ | ||
if ($readPreference === null || $readPreference->getModeString() === ReadPreference::PRIMARY) { | ||
return select_server($manager, $options); | ||
} | ||
|
@@ -635,3 +649,18 @@ function select_server_for_aggregate_write_stage(Manager $manager, array &$optio | |
|
||
return $server; | ||
} | ||
|
||
/** | ||
* Performs server selection for a write operation. | ||
* | ||
* The pinned server for an active transaction takes priority, followed by an | ||
* operation-level read preference, followed by a primary read preference. This | ||
* is similar to select_server() except that it ignores a read preference from | ||
* an active transaction's options. | ||
* | ||
* @internal | ||
*/ | ||
function select_server_for_write(Manager $manager, array $options): Server | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Quoting Transactions: readPreference:
The It's clear we don't have comprehensive test coverage for this across all operations (for example, there's no test that tries to list databases within a transaction), but I'm glad that I was alerted to this oversight in some failing legacy transaction spec tests. |
||
{ | ||
return select_server($manager, $options + ['readPreference' => new ReadPreference(ReadPreference::PRIMARY)]); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<?php | ||
|
||
namespace MongoDB\Tests\Functions; | ||
|
||
use MongoDB\Driver\ReadPreference; | ||
use MongoDB\Driver\Server; | ||
use MongoDB\Tests\FunctionalTestCase; | ||
|
||
use function MongoDB\select_server; | ||
|
||
class SelectServerFunctionalTest extends FunctionalTestCase | ||
{ | ||
/** @dataProvider providePinnedOptions */ | ||
public function testSelectServerPrefersPinnedServer(array $options): void | ||
{ | ||
$this->skipIfTransactionsAreNotSupported(); | ||
|
||
if (! $this->isShardedCluster()) { | ||
$this->markTestSkipped('Pinning requires a sharded cluster'); | ||
} | ||
|
||
if ($this->isLoadBalanced()) { | ||
$this->markTestSkipped('libmongoc does not pin for load-balanced topology'); | ||
} | ||
|
||
/* By default, the Manager under test is created with a single-mongos | ||
* URI. Explicitly create a Client with multiple mongoses. */ | ||
$client = static::createTestClient(static::getUri(true)); | ||
|
||
// Collection must be created before the transaction starts | ||
$this->createCollection($this->getDatabaseName(), $this->getCollectionName()); | ||
|
||
$session = $client->startSession(); | ||
$session->startTransaction(); | ||
|
||
$collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); | ||
$collection->find([], ['session' => $session]); | ||
|
||
$this->assertTrue($session->isInTransaction()); | ||
$this->assertInstanceOf(Server::class, $session->getServer(), 'Session is pinned'); | ||
$this->assertEquals($session->getServer(), select_server($client->getManager(), ['session' => $session])); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: this test is still a bit dubious, as we don't have any insight into what Currently, there's a chance that server selection just happens to select the same server that was pinned (see: Topology type: Sharded in the Server Selection spec). Supplying an alternative read preference in the operation-level options doesn't really affect this, since it wouldn't influence mongos selection. Again, logging would help verify that. Having said all that, I think adding a functional test is a good first step since we previously had no direct test coverage for this function. This file at least provides a starting point to add more coverage down the line. I've left a comment in PHPLIB-1000 to that effect. |
||
} | ||
|
||
public static function providePinnedOptions(): array | ||
{ | ||
return [ | ||
[['readPreference' => new ReadPreference(ReadPreference::PRIMARY_PREFERRED)]], | ||
[[]], | ||
]; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if that's relevant for search indexes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm familiar with
mongot
, but does that meanmongod
has absolutely not state when it comes to search indexes? Perhaps that's the case, aslistSearchIndexes()
is distinct fromlistIndexes()
.This also raises the question of what happens if the search index operations are executed within a transaction. Presumably they don't maintain state on
mongod
at all (and thus operate outside of a transaction), but it's not clear to me ifmongod
would actually restrict their usage during an active transaction.That said, I don't see the harm in requiring a primary in this case since it is a write operation from the user's perspective.