Skip to content

Commit 555625a

Browse files
author
Ace Nassri
authored
feat(functions): add infinite retries sample (GoogleCloudPlatform#1260)
1 parent bace72d commit 555625a

File tree

5 files changed

+360
-0
lines changed

5 files changed

+360
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"require": {
3+
"google/cloud-functions-framework": "^0.7.1"
4+
},
5+
"require-dev": {
6+
"google/cloud-pubsub": "^1.29",
7+
"google/cloud-logging": "^1.21"
8+
}
9+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
// [START functions_tips_infinite_retries]
19+
20+
/**
21+
* This function shows an example method for avoiding infinite retries in
22+
* Google Cloud Functions. By default, functions configured to automatically
23+
* retry execution on failure will be retried indefinitely - causing an
24+
* infinite loop. To avoid this, we stop retrying executions (by not throwing
25+
* exceptions) for any events that are older than a predefined threshold.
26+
*/
27+
28+
use Google\CloudFunctions\CloudEvent;
29+
30+
function avoidInfiniteRetries(CloudEvent $event): string
31+
{
32+
$log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb');
33+
34+
$eventId = $event->getId();
35+
36+
// The maximum age of events to process.
37+
$maxAge = 60 * 3; // 3 minutes, in seconds
38+
39+
// The age of the event being processed.
40+
$eventAge = time() - strtotime($event->getTime());
41+
42+
// Ignore events that are too old
43+
if ($eventAge > $maxAge) {
44+
fwrite($log, 'Dropping event ' . $eventId . ' with age ' . $eventAge . ' seconds' . PHP_EOL);
45+
return '';
46+
}
47+
48+
// Do what the function is supposed to do
49+
fwrite($log, 'Processing event: ' . $eventId . ' with age ' . $eventAge . ' seconds' . PHP_EOL);
50+
51+
// infinite_retries failed function executions
52+
$failed = true;
53+
if ($failed) {
54+
throw new Exception('Event ' . $eventId . ' failed; retrying...');
55+
}
56+
57+
return '';
58+
}
59+
// [END functions_tips_infinite_retries]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
xml version="1.0" encoding="UTF-8"?>
2+
17+
<phpunit bootstrap="../../testing/bootstrap.php" convertWarningsToExceptions="false">
18+
<testsuites>
19+
<testsuite name="Cloud Functions Tips Avoid Infinite Retries Test Suite">
20+
<directory>testdirectory>
21+
testsuite>
22+
testsuites>
23+
<logging>
24+
<log type="coverage-clover" target="build/logs/clover.xml"/>
25+
logging>
26+
<filter>
27+
<whitelist>
28+
<directory suffix=".php">.directory>
29+
<exclude>
30+
<directory>./vendordirectory>
31+
exclude>
32+
whitelist>
33+
filter>
34+
phpunit>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace Google\Cloud\Samples\Functions\TipsInfiniteRetries\Test;
21+
22+
use Google\Cloud\Logging\LoggingClient;
23+
use Google\Cloud\TestUtils\CloudFunctionDeploymentTrait;
24+
use Google\Cloud\TestUtils\EventuallyConsistentTestTrait;
25+
use Google\Cloud\TestUtils\GcloudWrapper\CloudFunction;
26+
use Google\Cloud\PubSub\PubSubClient;
27+
use PHPUnit\Framework\TestCase;
28+
use PHPUnit\Framework\ExpectationFailedException;
29+
30+
/**
31+
* Class DeployTest.
32+
*
33+
* This test is not run by the CI system.
34+
*
35+
* To skip deployment of a new function, run with "GOOGLE_SKIP_DEPLOYMENT=true".
36+
* To skip deletion of the tested function, run with "GOOGLE_KEEP_DEPLOYMENT=true".
37+
*/
38+
class DeployTest extends TestCase
39+
{
40+
use CloudFunctionDeploymentTrait;
41+
use EventuallyConsistentTestTrait;
42+
43+
private static $entryPoint = 'avoidInfiniteRetries';
44+
45+
/* var string */
46+
private static $projectId;
47+
48+
/* var string */
49+
private static $topicName;
50+
51+
/** @var LoggingClient */
52+
private static $loggingClient;
53+
54+
55+
public function testTipsRetry(): void
56+
{
57+
// Send Pub/Sub message.
58+
$this->publishMessage();
59+
60+
// Give event and log systems a head start.
61+
// If log retrieval fails to find logs for our function within retry limit, increase sleep time.
62+
sleep(30);
63+
64+
$fiveMinAgo = date(\DateTime::RFC3339, strtotime('-5 minutes'));
65+
$this->processFunctionLogs(self::$fn, $fiveMinAgo, function (\Iterator $logs) {
66+
// Concatenate all relevant log messages.
67+
$actual = '';
68+
foreach ($logs as $log) {
69+
$info = $log->info();
70+
$actual .= $info['textPayload'];
71+
}
72+
73+
// Check that multiple invocations of the function have occurred.
74+
$retryCount = substr_count($actual, 'retrying...');
75+
$this->assertGreaterThan(1, $retryCount);
76+
77+
// Check that the function has stopped retrying
78+
$this->assertContains('Dropping event', $actual);
79+
});
80+
}
81+
82+
private function publishMessage(): void
83+
{
84+
// Construct Pub/Sub message
85+
$message = json_encode(['retry' => true]);
86+
87+
// Publish a message to the function.
88+
$pubsub = new PubSubClient([
89+
'projectId' => self::$projectId,
90+
]);
91+
$topic = $pubsub->topic(self::$topicName);
92+
$topic->publish(['data' => $message]);
93+
}
94+
95+
/**
96+
* Retrieve and process logs for the defined function.
97+
*
98+
* @param CloudFunction $fn function whose logs should be checked.
99+
* @param string $startTime RFC3339 timestamp marking start of time range to retrieve.
100+
* @param callable $process callback function to run on the logs.
101+
*/
102+
private function processFunctionLogs(CloudFunction $fn, string $startTime, callable $process)
103+
{
104+
$projectId = self::requireEnv('GOOGLE_PROJECT_ID');
105+
106+
if (empty(self::$loggingClient)) {
107+
self::$loggingClient = new LoggingClient([
108+
'projectId' => $projectId
109+
]);
110+
}
111+
112+
// Define the log search criteria.
113+
$logFullName = 'projects/' . $projectId . '/logs/cloudfunctions.googleapis.com%2Fcloud-functions';
114+
$filter = sprintf(
115+
'logName="%s" resource.labels.function_name="%s" timestamp>="%s"',
116+
$logFullName,
117+
$fn->getFunctionName(),
118+
$startTime
119+
);
120+
121+
echo "\nRetrieving logs [$filter]... (this may take a minute or two)\n";
122+
123+
// Check for new logs for the function.
124+
$attempt = 1;
125+
$this->runEventuallyConsistentTest(function () use ($filter, $process, &$attempt) {
126+
$entries = self::$loggingClient->entries(['filter' => $filter]);
127+
128+
// If no logs came in try again.
129+
if (empty($entries->current())) {
130+
echo 'Logs not found, attempting retry #' . $attempt++ . PHP_EOL;
131+
throw new ExpectationFailedException('Log Entries not available');
132+
}
133+
echo 'Processing logs...' . PHP_EOL;
134+
135+
$process($entries);
136+
}, $retries = 10);
137+
}
138+
139+
/**
140+
* Deploy the Cloud Function, called from DeploymentTrait::deployApp().
141+
*
142+
* Overrides CloudFunctionDeploymentTrait::doDeploy().
143+
*/
144+
private static function doDeploy()
145+
{
146+
self::$projectId = self::requireEnv('GOOGLE_CLOUD_PROJECT');
147+
self::$topicName = self::requireEnv('FUNCTIONS_TOPIC');
148+
return self::$fn->deploy(['--retry' => ''], '--trigger-topic=' . self::$topicName);
149+
}
150+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace Google\Cloud\Samples\Functions\TipsInfiniteRetries\Test;
21+
22+
use PHPUnit\Framework\TestCase;
23+
use Google\Cloud\TestUtils\CloudFunctionLocalTestTrait;
24+
25+
/**
26+
* Class IntegrationTest.
27+
*
28+
* Integration Test for helloGCS.
29+
*/
30+
class IntegrationTest extends TestCase
31+
{
32+
use CloudFunctionLocalTestTrait;
33+
34+
/** @var string */
35+
private static $entryPoint = 'avoidInfiniteRetries';
36+
37+
/** @var string */
38+
private static $functionSignatureType = 'cloudevent';
39+
40+
41+
public function dataProvider()
42+
{
43+
return [
44+
[
45+
'cloudevent' => [
46+
'id' => 'new-event',
47+
'source' => 'pubsub.googleapis.com',
48+
'specversion' => '1.0',
49+
'type' => 'google.cloud.pubsub.topic.v1.messagePublished',
50+
'time' => gmdate('c', strtotime('+20 minutes'))
51+
],
52+
'data' => [],
53+
'statusCode' => '500',
54+
'expected' => 'Event new-event failed; retrying...',
55+
'label' => 'Should throw an exception to trigger a retry'
56+
],
57+
[
58+
'cloudevent' => [
59+
'id' => 'old-event',
60+
'source' => 'pubsub.googleapis.com',
61+
'specversion' => '1.0',
62+
'type' => 'google.cloud.pubsub.topic.v1.messagePublished',
63+
'time' => gmdate('c', strtotime('-20 minutes'))
64+
],
65+
'data' => [
66+
'data' => [],
67+
],
68+
'statusCode' => '200',
69+
'expected' => 'Dropping event old-event with age',
70+
'label' => 'Should not throw an exception if event is too old'
71+
],
72+
];
73+
}
74+
75+
/**
76+
* @dataProvider dataProvider
77+
*/
78+
public function testLimitInfiniteRetries(array $cloudevent, array $data, string $statusCode, string $expected, string $label): void
79+
{
80+
// Prepare the HTTP headers for a CloudEvent.
81+
$cloudEventHeaders = [];
82+
foreach ($cloudevent as $key => $value) {
83+
$cloudEventHeaders['ce-' . $key] = $value;
84+
}
85+
86+
// Send an HTTP request using CloudEvent metadata.
87+
$resp = $this->client->request('POST', '/', [
88+
'body' => json_encode($data),
89+
'headers' => $cloudEventHeaders + [
90+
// Instruct the function framework to parse the body as JSON.
91+
'content-type' => 'application/json'
92+
],
93+
]);
94+
95+
// The Cloud Function logs all data to stderr.
96+
$actual = self::$localhost->getIncrementalErrorOutput();
97+
98+
// Confirm the status code.
99+
$this->assertEquals(
100+
$statusCode,
101+
$resp->getStatusCode(),
102+
$label . ' status code'
103+
);
104+
105+
// Verify the function's behavior is correct.
106+
$this->assertContains($expected, $actual, $label . ' contains');
107+
}
108+
}

0 commit comments

Comments
 (0)