@@ -107,7 +107,7 @@ def test_propagation(enable_extended_tracing):
107107 gotNames = [span .name for span in from_inject_spans ]
108108 wantNames = [
109109 "CloudSpanner.CreateSession" ,
110- "CloudSpanner.Snapshot.execute_streaming_sql " ,
110+ "CloudSpanner.Snapshot.execute_sql " ,
111111 ]
112112 assert gotNames == wantNames
113113
@@ -216,8 +216,8 @@ def select_in_txn(txn):
216216 "CloudSpanner.Database.run_in_transaction" ,
217217 "CloudSpanner.CreateSession" ,
218218 "CloudSpanner.Session.run_in_transaction" ,
219- "CloudSpanner.Transaction.execute_streaming_sql " ,
220- "CloudSpanner.Transaction.execute_streaming_sql " ,
219+ "CloudSpanner.Transaction.execute_sql " ,
220+ "CloudSpanner.Transaction.execute_sql " ,
221221 "CloudSpanner.Transaction.commit" ,
222222 ]
223223
@@ -262,13 +262,207 @@ def select_in_txn(txn):
262262 ("CloudSpanner.Database.run_in_transaction" , codes .OK , None ),
263263 ("CloudSpanner.CreateSession" , codes .OK , None ),
264264 ("CloudSpanner.Session.run_in_transaction" , codes .OK , None ),
265- ("CloudSpanner.Transaction.execute_streaming_sql " , codes .OK , None ),
266- ("CloudSpanner.Transaction.execute_streaming_sql " , codes .OK , None ),
265+ ("CloudSpanner.Transaction.execute_sql " , codes .OK , None ),
266+ ("CloudSpanner.Transaction.execute_sql " , codes .OK , None ),
267267 ("CloudSpanner.Transaction.commit" , codes .OK , None ),
268268 ]
269269 assert got_statuses == want_statuses
270270
271271
272+ @pytest .mark .skipif (
273+ not _helpers .USE_EMULATOR ,
274+ reason = "Emulator needed to run this tests" ,
275+ )
276+ @pytest .mark .skipif (
277+ not HAS_OTEL_INSTALLED ,
278+ reason = "Tracing requires OpenTelemetry" ,
279+ )
280+ def test_transaction_update_implicit_begin_nested_inside_commit ():
281+ # Tests to ensure that transaction.commit() without a began transaction
282+ # has transaction.begin() inlined and nested under the commit span.
283+ from google .auth .credentials import AnonymousCredentials
284+ from google .api_core .exceptions import Aborted
285+ from google .rpc import code_pb2
286+ from opentelemetry .sdk .trace .export import SimpleSpanProcessor
287+ from opentelemetry .sdk .trace .export .in_memory_span_exporter import (
288+ InMemorySpanExporter ,
289+ )
290+ from opentelemetry .trace .status import StatusCode
291+ from opentelemetry .sdk .trace import TracerProvider
292+ from opentelemetry .sdk .trace .sampling import ALWAYS_ON
293+
294+ PROJECT = _helpers .EMULATOR_PROJECT
295+ CONFIGURATION_NAME = "config-name"
296+ INSTANCE_ID = _helpers .INSTANCE_ID
297+ DISPLAY_NAME = "display-name"
298+ DATABASE_ID = _helpers .unique_id ("temp_db" )
299+ NODE_COUNT = 5
300+ LABELS = {"test" : "true" }
301+
302+ counters = dict (aborted = 0 )
303+
304+ def tx_update (txn ):
305+ txn .update (
306+ "Singers" ,
307+ columns = ["SingerId" , "FirstName" ],
308+ values = [["1" , "Bryan" ], ["2" , "Slash" ]],
309+ )
310+
311+ tracer_provider = TracerProvider (sampler = ALWAYS_ON )
312+ trace_exporter = InMemorySpanExporter ()
313+ tracer_provider .add_span_processor (SimpleSpanProcessor (trace_exporter ))
314+ observability_options = dict (
315+ tracer_provider = tracer_provider ,
316+ enable_extended_tracing = True ,
317+ )
318+
319+ client = Client (
320+ project = PROJECT ,
321+ observability_options = observability_options ,
322+ credentials = AnonymousCredentials (),
323+ )
324+
325+ instance = client .instance (
326+ INSTANCE_ID ,
327+ CONFIGURATION_NAME ,
328+ display_name = DISPLAY_NAME ,
329+ node_count = NODE_COUNT ,
330+ labels = LABELS ,
331+ )
332+
333+ try :
334+ instance .create ()
335+ except Exception :
336+ pass
337+
338+ db = instance .database (DATABASE_ID )
339+ try :
340+ db ._ddl_statements = [
341+ """CREATE TABLE Singers (
342+ SingerId INT64 NOT NULL,
343+ FirstName STRING(1024),
344+ LastName STRING(1024),
345+ SingerInfo BYTES(MAX),
346+ FullName STRING(2048) AS (
347+ ARRAY_TO_STRING([FirstName, LastName], " ")
348+ ) STORED
349+ ) PRIMARY KEY (SingerId)""" ,
350+ """CREATE TABLE Albums (
351+ SingerId INT64 NOT NULL,
352+ AlbumId INT64 NOT NULL,
353+ AlbumTitle STRING(MAX),
354+ MarketingBudget INT64,
355+ ) PRIMARY KEY (SingerId, AlbumId),
356+ INTERLEAVE IN PARENT Singers ON DELETE CASCADE""" ,
357+ ]
358+ db .create ()
359+ except Exception :
360+ pass
361+
362+ try :
363+ db .run_in_transaction (tx_update )
364+ except :
365+ pass
366+
367+ span_list = trace_exporter .get_finished_spans ()
368+ # Sort the spans by their start time in the hierarchy.
369+ span_list = sorted (span_list , key = lambda span : span .start_time )
370+ got_span_names = [span .name for span in span_list ]
371+ want_span_names = [
372+ "CloudSpanner.Database.run_in_transaction" ,
373+ "CloudSpanner.CreateSession" ,
374+ "CloudSpanner.Session.run_in_transaction" ,
375+ "CloudSpanner.Transaction.commit" ,
376+ "CloudSpanner.Transaction.begin" ,
377+ ]
378+
379+ assert got_span_names == want_span_names
380+ span_tx_begin = span_list [- 1 ]
381+ span_tx_commit = span_list [- 2 ]
382+
383+ got_events = []
384+ got_statuses = []
385+
386+ # Some event attributes are noisy/highly ephemeral
387+ # and can't be directly compared against.
388+ imprecise_event_attributes = ["exception.stacktrace" , "delay_seconds" , "cause" ]
389+ for span in span_list :
390+ got_statuses .append (
391+ (span .name , span .status .status_code , span .status .description )
392+ )
393+ for event in span .events :
394+ evt_attributes = event .attributes .copy ()
395+ for attr_name in imprecise_event_attributes :
396+ if attr_name in evt_attributes :
397+ evt_attributes [attr_name ] = "EPHEMERAL"
398+
399+ got_events .append ((event .name , evt_attributes ))
400+
401+ # Check for the series of events
402+ want_events = [
403+ ("Acquiring session" , {"kind" : "BurstyPool" }),
404+ ("Waiting for a session to become available" , {"kind" : "BurstyPool" }),
405+ ("No sessions available in pool. Creating session" , {"kind" : "BurstyPool" }),
406+ ("Creating Session" , {}),
407+ (
408+ "exception" ,
409+ {
410+ "exception.type" : "google.api_core.exceptions.NotFound" ,
411+ "exception.message" : "404 Table Singers: Row {Int64(1)} not found." ,
412+ "exception.stacktrace" : "EPHEMERAL" ,
413+ "exception.escaped" : "False" ,
414+ },
415+ ),
416+ (
417+ "Transaction.commit failed due to GoogleAPICallError, not retrying" ,
418+ {"attempt" : 1 },
419+ ),
420+ (
421+ "exception" ,
422+ {
423+ "exception.type" : "google.api_core.exceptions.NotFound" ,
424+ "exception.message" : "404 Table Singers: Row {Int64(1)} not found." ,
425+ "exception.stacktrace" : "EPHEMERAL" ,
426+ "exception.escaped" : "False" ,
427+ },
428+ ),
429+ ("Starting Commit" , {}),
430+ (
431+ "exception" ,
432+ {
433+ "exception.type" : "google.api_core.exceptions.NotFound" ,
434+ "exception.message" : "404 Table Singers: Row {Int64(1)} not found." ,
435+ "exception.stacktrace" : "EPHEMERAL" ,
436+ "exception.escaped" : "False" ,
437+ },
438+ ),
439+ ]
440+ assert got_events == want_events
441+
442+ # Check for the statues.
443+ codes = StatusCode
444+ want_statuses = [
445+ (
446+ "CloudSpanner.Database.run_in_transaction" ,
447+ codes .ERROR ,
448+ "NotFound: 404 Table Singers: Row {Int64(1)} not found." ,
449+ ),
450+ ("CloudSpanner.CreateSession" , codes .OK , None ),
451+ (
452+ "CloudSpanner.Session.run_in_transaction" ,
453+ codes .ERROR ,
454+ "NotFound: 404 Table Singers: Row {Int64(1)} not found." ,
455+ ),
456+ (
457+ "CloudSpanner.Transaction.commit" ,
458+ codes .ERROR ,
459+ "NotFound: 404 Table Singers: Row {Int64(1)} not found." ,
460+ ),
461+ ("CloudSpanner.Transaction.begin" , codes .OK , None ),
462+ ]
463+ assert got_statuses == want_statuses
464+
465+
272466def _make_credentials ():
273467 from google .auth .credentials import AnonymousCredentials
274468
0 commit comments