Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[Spanner] Buffered mutations are committed twice on the multiplexed-session precommit-token retry (#9331 fix covers inline-begin mutation-only transactions only)

Aperta
#9,739 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
google-cloud, php
Ambito
databases

Direzione di ricerca

Start in src/Operation.php around commit() and executeUpdateBatch(), then inspect src/Result.php around Result::createGenerator() and compare the Go client's multiplexed-session retry behavior. Reproduce the listed transaction shapes against the emulator and add regression coverage for mutation-only and statement-before-mutation transactions. Done means commits do not duplicate buffered mutations or raise ALREADY_EXISTS.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Inserting data using the mutation API inside a read-write transaction causes the record to be inserted twice, unless the transaction is an inline-begin transaction that buffers mutations and does nothing else. That one case was #9331, fixed by #9406. A transaction that runs any statement before buffering, or one begun explicitly with $database->transaction(), still fails on v2.11.0 with the same ALREADY_EXISTS.

I believe this is the precommit-token retry in Operation::commit(). On a multiplexed session the emulator answers the first Commit that carries no precommit token by buffering the request's mutations and returning a token instead of committing. The loop below then re-sends the same $commitRequest, mutations included, and the emulator writes them a second time.

https://github.com/googleapis/google-cloud-php-spanner/blob/c4b0d2f4700c4fee8ccd315d9c8c2c9a1826e91d/src/Operation.php#L173-L182

The Go client retries this case without the mutations (spanner/transaction.go: "Retry if MultiplexedSessionRetry is present, without mutations"; performCommit(false)).

The commit has no token to carry because the emulator's ExecuteSql/ExecuteStreamingSql responses for these transactions contain an empty precommit_token message, and Result::createGenerator() keeps a token only when its bytes are present:

https://github.com/googleapis/google-cloud-php-spanner/blob/c4b0d2f4700c4fee8ccd315d9c8c2c9a1826e91d/src/Result.php#L523

Operation::executeUpdateBatch() keeps the message object regardless, which is why a transaction that ran batch DML commits fine — its commit already carries a token and never enters the retry:

https://github.com/googleapis/google-cloud-php-spanner/blob/c4b0d2f4700c4fee8ccd315d9c8c2c9a1826e91d/src/Operation.php#L491

Shapes tried, counting the buffered row after the transaction:

shape result
inline begin, insertBatch only, commit 1 row (the case #9406 fixed)
inline begin, executeUpdate then insertBatch, commit ALREADY_EXISTS, 0 rows
inline begin, execute('SELECT 1') then insertBatch, commit ALREADY_EXISTS, 0 rows
explicit $database->transaction(), insertBatch only, commit ALREADY_EXISTS, 0 rows
explicit begin, executeUpdate then insertBatch, commit ALREADY_EXISTS, 0 rows
any of the above with executeUpdateBatch in place of executeUpdate 1 row
insertOrUpdateBatch or replaceBatch in place of insertBatch, with a commit-timestamp column error: the pending commit timestamp cannot be read (the row is written twice and the second write reads the first)

I think either of these would fix it, and both would be consistent with the other clients: on the precommit-token retry, send the commit without mutations; and in Result::createGenerator(), keep the precommit token whenever the message is present, not only when its bytes are non-empty, as executeUpdateBatch() already does. Happy to test a patch against the emulator.

Environment details
  • OS: Linux (Docker, official php:8.5-fpm image)
  • PHP version: 8.5.8
  • Package name and version: google/cloud-spanner v2.11.0 (also reproduced on v2.10.0); google/cloud-core v1.73.4
  • Cloud Spanner emulator gcr.io/cloud-spanner-emulator/emulator@sha256:c6f3402f2599684f295a0fdefb6fbbbfb18a0e43e309ff5456ccb452a4570a79 (built 2026-09-15)
Code example
<?php

require __DIR__ . '/vendor/autoload.php';

use Google\Cloud\Spanner\SpannerClient;
use Google\Cloud\Spanner\Transaction;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

$projectId  = getenv('DB_SPANNER_PROJECT_ID')  ?: 'test-project';
$instanceId = getenv('DB_SPANNER_INSTANCE_ID') ?: 'test-instance';
$databaseId = getenv('DB_SPANNER_DATABASE_ID') ?: 'test-db';

$client = new SpannerClient([
    'projectId' => $projectId,
    'cacheItemPool' => new ArrayAdapter(),
]);

$instance = $client->instance($instanceId);

if (!$instance->exists()) {
    $config = $client->instanceConfiguration('emulator-config');
    $instance->create($config)->pollUntilComplete();
}

$database = $instance->database($databaseId);
$table = 'MutationTest';

if (!$database->exists()) {
    $instance->createDatabase($databaseId, [
        'statements' => [
            "CREATE TABLE $table (id INT64 NOT NULL, name STRING(10)) PRIMARY KEY (id)",
        ],
    ])->pollUntilComplete();
}

$database->runTransaction(function (Transaction $tx) use ($table) {
    // Any statement before the mutation is enough: DML here, a SELECT does the same.
    $tx->executeUpdate("INSERT INTO $table (id, name) VALUES (@id, 'dml')", ['parameters' => ['id' => rand()]]);
    $tx->insertBatch($table, [['id' => rand(), 'name' => 'test']]);
    $tx->commit();
});

The above code will throw the following error, and neither row is in the table afterwards.

Fatal error: Uncaught Google\Cloud\Core\Exception\ConflictException: {
    "message": "Table MutationTest: Row {Int64(1653590878)} already exists.",
    "code": 6,
    "status": "ALREADY_EXISTS",
    "details": [
        {
            "@type": "google.spanner.ConstraintError",
            "typeUrl": "google.spanner.ConstraintError",
            "value": ""
        }
    ]
} in /project/vendor/google/cloud-core/src/RequestProcessorTrait.php:149
Lingua principale
PHP
Stelle
1.2k
Fork
464
Merge medio
2g 2h
PR unite (30g)
84

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di googleapis/google-cloud-php

Tutte le issue di googleapis/google-cloud-php

Issue simili

Altre issue su PHP

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.