Skip to content

Commit 191cb3a

Browse files
committed
feat(workflows): modernize samples and add idempotency samples
The Workflows Python samples only ran executions: they covered 2 of the 10 client methods, defined one region tag in two files, used TODO placeholders, and pinned an old library version. The new idempotency tutorial also needs a Python sample that's safe to retry. Client-library samples (workflows/cloud-client), using google-cloud-workflows 1.24.0: - Updated: workflows_execute_without_arguments, workflows_execute_with_arguments, workflows_api_quickstart (main.py, matching the Node.js tab; the duplicate tag and unused sub-tags are removed). File paths are unchanged, because docs embed them by path. - New: - workflows_client_library (list workflows, matching Node.js and C++) - workflows_create_workflow - workflows_get_workflow - workflows_update_workflow - workflows_delete_workflow - workflows_get_execution - workflows_list_executions (uses the FULL view, which includes errors) - workflows_cancel_execution Idempotency samples, each a Cloud Run function backed by Firestore: - idempotent_payment: workflows_idempotent_process_payment and workflows_idempotent_order_workflow, for the "Make API calls idempotent" tutorial. - The function claims an order with a lease and an attempt ID, then charges outside the transaction with the order ID as an idempotency key. - Orders have explicit states. - On a duplicate request, the workflow resumes from the stored state instead of exiting. - deduplicate_scheduled_runs: workflows_deduplicate_scheduled_runs starts a workflow once per Cloud Scheduler run. It keys on the scheduled minute, because a forced run can send the schedule time without Cloud Scheduler's per-job offset. - order_saga: workflows_order_saga_workflow and workflows_order_saga_refund refund a charge once when fulfillment fails. Tests live in tests/ subdirectories: - Unit tests run without credentials. - Integration tests are skipped unless RUN_INTEGRATION_TESTS is set. They cover: - duplicate and concurrent executions - partial failures - ambiguous failures and timeouts at two tiers - crashed attempts - Cloud Scheduler duplicate deliveries - saga compensation Each one checks that a stand-in payment service charged exactly once. Unit and integration tests pass on Python 3.13.
1 parent f70bd51 commit 191cb3a

67 files changed

Lines changed: 4378 additions & 396 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START workflows_cancel_execution]
16+
import sys
17+
18+
from google.api_core import exceptions
19+
from google.cloud.workflows import executions_v1
20+
21+
22+
def cancel_execution(
23+
project_id: str, location_id: str, workflow_id: str, execution_id: str
24+
) -> None:
25+
"""Cancels an execution that's still running.
26+
27+
Args:
28+
project_id: The Google Cloud project ID (for example, 'example-project-id').
29+
location_id: The region of the workflow (for example, 'us-central1').
30+
workflow_id: The ID of the workflow (for example, 'myFirstWorkflow').
31+
execution_id: The ID of the execution.
32+
"""
33+
client = executions_v1.ExecutionsClient()
34+
name = (
35+
f"projects/{project_id}/locations/{location_id}/workflows/{workflow_id}"
36+
f"/executions/{execution_id}"
37+
)
38+
39+
try:
40+
execution = client.cancel_execution(name=name)
41+
except exceptions.NotFound as err:
42+
print(
43+
f"error: execution '{execution_id}' not found "
44+
f"for workflow '{workflow_id}' in location '{location_id}': {err.message}",
45+
file=sys.stderr,
46+
)
47+
except exceptions.GoogleAPICallError as err:
48+
print(
49+
f"error: failed to cancel execution '{execution_id}' "
50+
f"for workflow '{workflow_id}' in location '{location_id}': {err.message}",
51+
file=sys.stderr,
52+
)
53+
else:
54+
print(f"Resource ID: {execution.name}")
55+
print(f"End Time: {execution.end_time}")
56+
print(f"State: {execution.state.name}")
57+
58+
59+
# [END workflows_cancel_execution]
60+
61+
62+
if __name__ == "__main__":
63+
import argparse
64+
import os
65+
66+
# Each flag defaults to its environment variable, then to an example value.
67+
parser = argparse.ArgumentParser(
68+
description=cancel_execution.__doc__.splitlines()[0]
69+
)
70+
parser.add_argument(
71+
"--project-id",
72+
default=os.environ.get("GOOGLE_CLOUD_PROJECT", "example-project-id"),
73+
help="Google Cloud project ID (default: $GOOGLE_CLOUD_PROJECT)",
74+
)
75+
parser.add_argument(
76+
"--location-id",
77+
default=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1"),
78+
help="Region of the workflow (default: $GOOGLE_CLOUD_LOCATION)",
79+
)
80+
parser.add_argument(
81+
"--workflow-id",
82+
default=os.environ.get("WORKFLOW_ID", "myFirstWorkflow"),
83+
help="ID of the workflow (default: $WORKFLOW_ID)",
84+
)
85+
parser.add_argument(
86+
"--execution-id",
87+
default=os.environ.get("EXECUTION_ID", "example-execution-id"),
88+
help="ID of the execution (default: $EXECUTION_ID)",
89+
)
90+
args = parser.parse_args()
91+
92+
cancel_execution(
93+
project_id=args.project_id,
94+
location_id=args.location_id,
95+
workflow_id=args.workflow_id,
96+
execution_id=args.execution_id,
97+
)

‎workflows/cloud-client/conftest.py‎

Lines changed: 0 additions & 104 deletions
This file was deleted.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START workflows_create_workflow]
16+
import sys
17+
18+
from google.api_core import exceptions
19+
from google.cloud import workflows_v1
20+
21+
22+
def create_workflow(
23+
project_id: str, location_id: str, workflow_id: str, service_account_email: str
24+
) -> None:
25+
"""Creates a workflow with logging, labels, and environment variables.
26+
27+
Args:
28+
project_id: The Google Cloud project ID (for example, 'example-project-id').
29+
location_id: The region of the workflow (for example, 'us-central1').
30+
workflow_id: The ID for the new workflow (for example, 'example-workflow').
31+
service_account_email: The service account that the workflow runs as.
32+
"""
33+
client = workflows_v1.WorkflowsClient()
34+
parent = f"projects/{project_id}/locations/{location_id}"
35+
36+
# The workflow definition, in YAML. It returns a greeting from an
37+
# environment variable that's set on the workflow below.
38+
source_contents = """
39+
main:
40+
steps:
41+
- greet:
42+
return: ${sys.get_env("EXAMPLE_GREETING")}
43+
"""
44+
workflow = workflows_v1.Workflow(
45+
source_contents=source_contents,
46+
service_account=service_account_email,
47+
# Logs each call that fails, without logging successful calls.
48+
call_log_level=workflows_v1.Workflow.CallLogLevel.LOG_ERRORS_ONLY,
49+
labels={"environment": "example"},
50+
user_env_vars={"EXAMPLE_GREETING": "example-greeting"},
51+
)
52+
53+
try:
54+
operation = client.create_workflow(
55+
parent=parent, workflow=workflow, workflow_id=workflow_id
56+
)
57+
print("Waiting for the workflow to deploy...")
58+
response = operation.result(timeout=300)
59+
except exceptions.AlreadyExists as err:
60+
print(
61+
f"error: workflow '{workflow_id}' already exists "
62+
f"in location '{location_id}' for project '{project_id}': {err.message}",
63+
file=sys.stderr,
64+
)
65+
except exceptions.GoogleAPICallError as err:
66+
print(
67+
f"error: failed to create workflow '{workflow_id}' "
68+
f"in location '{location_id}' for project '{project_id}': {err.message}",
69+
file=sys.stderr,
70+
)
71+
else:
72+
print(f"Resource ID: {response.name}")
73+
print(f"Creation Date: {response.create_time}")
74+
print(f"Call Log Level: {response.call_log_level.name}")
75+
print(f"Revision ID: {response.revision_id}")
76+
print(f"State: {response.state.name}")
77+
78+
79+
# [END workflows_create_workflow]
80+
81+
82+
if __name__ == "__main__":
83+
import argparse
84+
import os
85+
86+
# Each flag defaults to its environment variable, then to an example value.
87+
parser = argparse.ArgumentParser(
88+
description=create_workflow.__doc__.splitlines()[0]
89+
)
90+
parser.add_argument(
91+
"--project-id",
92+
default=os.environ.get("GOOGLE_CLOUD_PROJECT", "example-project-id"),
93+
help="Google Cloud project ID (default: $GOOGLE_CLOUD_PROJECT)",
94+
)
95+
parser.add_argument(
96+
"--location-id",
97+
default=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1"),
98+
help="Region of the workflow (default: $GOOGLE_CLOUD_LOCATION)",
99+
)
100+
parser.add_argument(
101+
"--workflow-id",
102+
default=os.environ.get("WORKFLOW_ID", "example-workflow"),
103+
help="ID for the new workflow (default: $WORKFLOW_ID)",
104+
)
105+
parser.add_argument(
106+
"--service-account-email",
107+
default=os.environ.get(
108+
"SERVICE_ACCOUNT_EMAIL",
109+
"example-runner@example-project-id.iam.gserviceaccount.com",
110+
),
111+
help="Service account the workflow runs as (default: $SERVICE_ACCOUNT_EMAIL)",
112+
)
113+
args = parser.parse_args()
114+
115+
create_workflow(
116+
project_id=args.project_id,
117+
location_id=args.location_id,
118+
workflow_id=args.workflow_id,
119+
service_account_email=args.service_account_email,
120+
)

0 commit comments

Comments
 (0)