Skip to content

Commit f32542d

Browse files
authored
PHPLIB-476: Consider transaction readPreference in select_server (#1178)
This also refactors the conditionals in extract_session_from_options and extract_read_preference_from_options to improve readability. select_server() previously did not consider the read preference of an active transaction. This isn't very significant, as transactions require a primary read preference, but it is correct to do so. This also introduces select_server_for_write() to avoid inheriting an active transaction's readPreference option.
1 parent 74d19f8 commit f32542d

File tree

5 files changed

+119
-39
lines changed

5 files changed

+119
-39
lines changed

src/Client.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public function dropDatabase(string $databaseName, array $options = [])
200200
$options['typeMap'] = $this->typeMap;
201201
}
202202

203-
$server = select_server($this->manager, $options);
203+
$server = select_server_for_write($this->manager, $options);
204204

205205
if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
206206
$options['writeConcern'] = $this->writeConcern;

src/Collection.php

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ public function bulkWrite(array $operations, array $options = [])
260260

261261
$operation = new BulkWrite($this->databaseName, $this->collectionName, $operations, $options);
262262

263-
return $operation->execute(select_server($this->manager, $options));
263+
return $operation->execute(select_server_for_write($this->manager, $options));
264264
}
265265

266266
/**
@@ -362,7 +362,7 @@ public function createIndexes(array $indexes, array $options = [])
362362

363363
$operation = new CreateIndexes($this->databaseName, $this->collectionName, $indexes, $options);
364364

365-
return $operation->execute(select_server($this->manager, $options));
365+
return $operation->execute(select_server_for_write($this->manager, $options));
366366
}
367367

368368
/**
@@ -418,7 +418,7 @@ public function createSearchIndex($definition, array $options = []): string
418418
public function createSearchIndexes(array $indexes, array $options = []): array
419419
{
420420
$operation = new CreateSearchIndexes($this->databaseName, $this->collectionName, $indexes, $options);
421-
$server = select_server($this->manager, $options);
421+
$server = select_server_for_write($this->manager, $options);
422422

423423
return $operation->execute($server);
424424
}
@@ -441,7 +441,7 @@ public function deleteMany($filter, array $options = [])
441441

442442
$operation = new DeleteMany($this->databaseName, $this->collectionName, $filter, $options);
443443

444-
return $operation->execute(select_server($this->manager, $options));
444+
return $operation->execute(select_server_for_write($this->manager, $options));
445445
}
446446

447447
/**
@@ -462,7 +462,7 @@ public function deleteOne($filter, array $options = [])
462462

463463
$operation = new DeleteOne($this->databaseName, $this->collectionName, $filter, $options);
464464

465-
return $operation->execute(select_server($this->manager, $options));
465+
return $operation->execute(select_server_for_write($this->manager, $options));
466466
}
467467

468468
/**
@@ -503,7 +503,7 @@ public function drop(array $options = [])
503503
$options = $this->inheritWriteOptions($options);
504504
$options = $this->inheritTypeMap($options);
505505

506-
$server = select_server($this->manager, $options);
506+
$server = select_server_for_write($this->manager, $options);
507507

508508
if (! isset($options['encryptedFields'])) {
509509
$options['encryptedFields'] = get_encrypted_fields_from_driver($this->databaseName, $this->collectionName, $this->manager)
@@ -541,7 +541,7 @@ public function dropIndex($indexName, array $options = [])
541541

542542
$operation = new DropIndexes($this->databaseName, $this->collectionName, $indexName, $options);
543543

544-
return $operation->execute(select_server($this->manager, $options));
544+
return $operation->execute(select_server_for_write($this->manager, $options));
545545
}
546546

547547
/**
@@ -561,7 +561,7 @@ public function dropIndexes(array $options = [])
561561

562562
$operation = new DropIndexes($this->databaseName, $this->collectionName, '*', $options);
563563

564-
return $operation->execute(select_server($this->manager, $options));
564+
return $operation->execute(select_server_for_write($this->manager, $options));
565565
}
566566

567567
/**
@@ -577,7 +577,7 @@ public function dropIndexes(array $options = [])
577577
public function dropSearchIndex(string $name, array $options = []): void
578578
{
579579
$operation = new DropSearchIndex($this->databaseName, $this->collectionName, $name);
580-
$server = select_server($this->manager, $options);
580+
$server = select_server_for_write($this->manager, $options);
581581

582582
$operation->execute($server);
583583
}
@@ -690,7 +690,7 @@ public function findOneAndDelete($filter, array $options = [])
690690

691691
$operation = new FindOneAndDelete($this->databaseName, $this->collectionName, $filter, $options);
692692

693-
return $operation->execute(select_server($this->manager, $options));
693+
return $operation->execute(select_server_for_write($this->manager, $options));
694694
}
695695

696696
/**
@@ -720,7 +720,7 @@ public function findOneAndReplace($filter, $replacement, array $options = [])
720720

721721
$operation = new FindOneAndReplace($this->databaseName, $this->collectionName, $filter, $replacement, $options);
722722

723-
return $operation->execute(select_server($this->manager, $options));
723+
return $operation->execute(select_server_for_write($this->manager, $options));
724724
}
725725

726726
/**
@@ -750,7 +750,7 @@ public function findOneAndUpdate($filter, $update, array $options = [])
750750

751751
$operation = new FindOneAndUpdate($this->databaseName, $this->collectionName, $filter, $update, $options);
752752

753-
return $operation->execute(select_server($this->manager, $options));
753+
return $operation->execute(select_server_for_write($this->manager, $options));
754754
}
755755

756756
/**
@@ -854,7 +854,7 @@ public function insertMany(array $documents, array $options = [])
854854

855855
$operation = new InsertMany($this->databaseName, $this->collectionName, $documents, $options);
856856

857-
return $operation->execute(select_server($this->manager, $options));
857+
return $operation->execute(select_server_for_write($this->manager, $options));
858858
}
859859

860860
/**
@@ -875,7 +875,7 @@ public function insertOne($document, array $options = [])
875875

876876
$operation = new InsertOne($this->databaseName, $this->collectionName, $document, $options);
877877

878-
return $operation->execute(select_server($this->manager, $options));
878+
return $operation->execute(select_server_for_write($this->manager, $options));
879879
}
880880

881881
/**
@@ -949,7 +949,7 @@ public function mapReduce(JavascriptInterface $map, JavascriptInterface $reduce,
949949

950950
$operation = new MapReduce($this->databaseName, $this->collectionName, $map, $reduce, $out, $options);
951951

952-
return $operation->execute(select_server($this->manager, $options));
952+
return $operation->execute(select_server_for_write($this->manager, $options));
953953
}
954954

955955
/**
@@ -975,7 +975,7 @@ public function rename(string $toCollectionName, ?string $toDatabaseName = null,
975975

976976
$operation = new RenameCollection($this->databaseName, $this->collectionName, $toDatabaseName, $toCollectionName, $options);
977977

978-
return $operation->execute(select_server($this->manager, $options));
978+
return $operation->execute(select_server_for_write($this->manager, $options));
979979
}
980980

981981
/**
@@ -998,7 +998,7 @@ public function replaceOne($filter, $replacement, array $options = [])
998998

999999
$operation = new ReplaceOne($this->databaseName, $this->collectionName, $filter, $replacement, $options);
10001000

1001-
return $operation->execute(select_server($this->manager, $options));
1001+
return $operation->execute(select_server_for_write($this->manager, $options));
10021002
}
10031003

10041004
/**
@@ -1020,7 +1020,7 @@ public function updateMany($filter, $update, array $options = [])
10201020

10211021
$operation = new UpdateMany($this->databaseName, $this->collectionName, $filter, $update, $options);
10221022

1023-
return $operation->execute(select_server($this->manager, $options));
1023+
return $operation->execute(select_server_for_write($this->manager, $options));
10241024
}
10251025

10261026
/**
@@ -1042,7 +1042,7 @@ public function updateOne($filter, $update, array $options = [])
10421042

10431043
$operation = new UpdateOne($this->databaseName, $this->collectionName, $filter, $update, $options);
10441044

1045-
return $operation->execute(select_server($this->manager, $options));
1045+
return $operation->execute(select_server_for_write($this->manager, $options));
10461046
}
10471047

10481048
/**
@@ -1059,7 +1059,7 @@ public function updateOne($filter, $update, array $options = [])
10591059
public function updateSearchIndex(string $name, $definition, array $options = []): void
10601060
{
10611061
$operation = new UpdateSearchIndex($this->databaseName, $this->collectionName, $name, $definition, $options);
1062-
$server = select_server($this->manager, $options);
1062+
$server = select_server_for_write($this->manager, $options);
10631063

10641064
$operation->execute($server);
10651065
}

src/Database.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ public function createCollection(string $collectionName, array $options = [])
282282
? new CreateEncryptedCollection($this->databaseName, $collectionName, $options)
283283
: new CreateCollection($this->databaseName, $collectionName, $options);
284284

285-
$server = select_server($this->manager, $options);
285+
$server = select_server_for_write($this->manager, $options);
286286

287287
return $operation->execute($server);
288288
}
@@ -318,7 +318,7 @@ public function createEncryptedCollection(string $collectionName, ClientEncrypti
318318
}
319319

320320
$operation = new CreateEncryptedCollection($this->databaseName, $collectionName, $options);
321-
$server = select_server($this->manager, $options);
321+
$server = select_server_for_write($this->manager, $options);
322322

323323
try {
324324
$operation->createDataKeys($clientEncryption, $kmsProvider, $masterKey, $encryptedFields);
@@ -346,7 +346,7 @@ public function drop(array $options = [])
346346
$options['typeMap'] = $this->typeMap;
347347
}
348348

349-
$server = select_server($this->manager, $options);
349+
$server = select_server_for_write($this->manager, $options);
350350

351351
if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
352352
$options['writeConcern'] = $this->writeConcern;
@@ -374,7 +374,7 @@ public function dropCollection(string $collectionName, array $options = [])
374374
$options['typeMap'] = $this->typeMap;
375375
}
376376

377-
$server = select_server($this->manager, $options);
377+
$server = select_server_for_write($this->manager, $options);
378378

379379
if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
380380
$options['writeConcern'] = $this->writeConcern;
@@ -502,7 +502,7 @@ public function modifyCollection(string $collectionName, array $collectionOption
502502
$options['typeMap'] = $this->typeMap;
503503
}
504504

505-
$server = select_server($this->manager, $options);
505+
$server = select_server_for_write($this->manager, $options);
506506

507507
if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
508508
$options['writeConcern'] = $this->writeConcern;
@@ -536,7 +536,7 @@ public function renameCollection(string $fromCollectionName, string $toCollectio
536536
$options['typeMap'] = $this->typeMap;
537537
}
538538

539-
$server = select_server($this->manager, $options);
539+
$server = select_server_for_write($this->manager, $options);
540540

541541
if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
542542
$options['writeConcern'] = $this->writeConcern;

src/functions.php

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -544,11 +544,11 @@ function with_transaction(Session $session, callable $callback, array $transacti
544544
*/
545545
function extract_session_from_options(array $options): ?Session
546546
{
547-
if (! isset($options['session']) || ! $options['session'] instanceof Session) {
548-
return null;
547+
if (isset($options['session']) && $options['session'] instanceof Session) {
548+
return $options['session'];
549549
}
550550

551-
return $options['session'];
551+
return null;
552552
}
553553

554554
/**
@@ -558,33 +558,43 @@ function extract_session_from_options(array $options): ?Session
558558
*/
559559
function extract_read_preference_from_options(array $options): ?ReadPreference
560560
{
561-
if (! isset($options['readPreference']) || ! $options['readPreference'] instanceof ReadPreference) {
562-
return null;
561+
if (isset($options['readPreference']) && $options['readPreference'] instanceof ReadPreference) {
562+
return $options['readPreference'];
563563
}
564564

565-
return $options['readPreference'];
565+
return null;
566566
}
567567

568568
/**
569-
* Performs server selection, respecting the readPreference and session options
570-
* (if given)
569+
* Performs server selection, respecting the readPreference and session options.
570+
*
571+
* The pinned server for an active transaction takes priority, followed by an
572+
* operation-level read preference, followed by an active transaction's read
573+
* preference, followed by a primary read preference.
571574
*
572575
* @internal
573576
*/
574577
function select_server(Manager $manager, array $options): Server
575578
{
576579
$session = extract_session_from_options($options);
577580
$server = $session instanceof Session ? $session->getServer() : null;
581+
582+
// Pinned server for an active transaction takes priority
578583
if ($server !== null) {
579584
return $server;
580585
}
581586

587+
// Operation read preference takes priority
582588
$readPreference = extract_read_preference_from_options($options);
583-
if (! $readPreference instanceof ReadPreference) {
584-
// TODO: PHPLIB-476: Read transaction read preference once PHPC-1439 is implemented
585-
$readPreference = new ReadPreference(ReadPreference::PRIMARY);
589+
590+
// Read preference for an active transaction takes priority
591+
if ($readPreference === null && $session instanceof Session && $session->isInTransaction()) {
592+
/* Session::getTransactionOptions() should always return an array if the
593+
* session is in a transaction, but we can be defensive. */
594+
$readPreference = extract_read_preference_from_options($session->getTransactionOptions() ?? []);
586595
}
587596

597+
// Manager::selectServer() defaults to a primary read preference
588598
return $manager->selectServer($readPreference);
589599
}
590600

@@ -601,7 +611,11 @@ function select_server_for_aggregate_write_stage(Manager $manager, array &$optio
601611
$readPreference = extract_read_preference_from_options($options);
602612

603613
/* If there is either no read preference or a primary read preference, there
604-
* is no special server selection logic to apply. */
614+
* is no special server selection logic to apply.
615+
*
616+
* Note: an alternative read preference could still be inherited from an
617+
* active transaction's options, but we can rely on libmongoc to raise a
618+
* "read preference in a transaction must be primary" error if necessary. */
605619
if ($readPreference === null || $readPreference->getModeString() === ReadPreference::PRIMARY) {
606620
return select_server($manager, $options);
607621
}
@@ -635,3 +649,18 @@ function select_server_for_aggregate_write_stage(Manager $manager, array &$optio
635649

636650
return $server;
637651
}
652+
653+
/**
654+
* Performs server selection for a write operation.
655+
*
656+
* The pinned server for an active transaction takes priority, followed by an
657+
* operation-level read preference, followed by a primary read preference. This
658+
* is similar to select_server() except that it ignores a read preference from
659+
* an active transaction's options.
660+
*
661+
* @internal
662+
*/
663+
function select_server_for_write(Manager $manager, array $options): Server
664+
{
665+
return select_server($manager, $options + ['readPreference' => new ReadPreference(ReadPreference::PRIMARY)]);
666+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\Functions;
4+
5+
use MongoDB\Driver\ReadPreference;
6+
use MongoDB\Driver\Server;
7+
use MongoDB\Tests\FunctionalTestCase;
8+
9+
use function MongoDB\select_server;
10+
11+
class SelectServerFunctionalTest extends FunctionalTestCase
12+
{
13+
/** @dataProvider providePinnedOptions */
14+
public function testSelectServerPrefersPinnedServer(array $options): void
15+
{
16+
$this->skipIfTransactionsAreNotSupported();
17+
18+
if (! $this->isShardedCluster()) {
19+
$this->markTestSkipped('Pinning requires a sharded cluster');
20+
}
21+
22+
if ($this->isLoadBalanced()) {
23+
$this->markTestSkipped('libmongoc does not pin for load-balanced topology');
24+
}
25+
26+
/* By default, the Manager under test is created with a single-mongos
27+
* URI. Explicitly create a Client with multiple mongoses. */
28+
$client = static::createTestClient(static::getUri(true));
29+
30+
// Collection must be created before the transaction starts
31+
$this->createCollection($this->getDatabaseName(), $this->getCollectionName());
32+
33+
$session = $client->startSession();
34+
$session->startTransaction();
35+
36+
$collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName());
37+
$collection->find([], ['session' => $session]);
38+
39+
$this->assertTrue($session->isInTransaction());
40+
$this->assertInstanceOf(Server::class, $session->getServer(), 'Session is pinned');
41+
$this->assertEquals($session->getServer(), select_server($client->getManager(), ['session' => $session]));
42+
}
43+
44+
public static function providePinnedOptions(): array
45+
{
46+
return [
47+
[['readPreference' => new ReadPreference(ReadPreference::PRIMARY_PREFERRED)]],
48+
[[]],
49+
];
50+
}
51+
}

0 commit comments

Comments
 (0)