[cloud functions] Fix race condition when counting received chunks
Change-Id: I7784792801d71e2806303958d7b54a388c4db73c
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/130801
Reviewed-by: Alexander Thomas <athom@google.com>
diff --git a/functions/node/firestore_impl.dart b/functions/node/firestore_impl.dart
index 4ce1032..8e9bf75 100644
--- a/functions/node/firestore_impl.dart
+++ b/functions/node/firestore_impl.dart
@@ -88,8 +88,13 @@
final documentRef = firestore.document('builds/$builder:$index');
final record = await documentRef.get();
if (!record.exists) {
- await documentRef.setData(DocumentData.fromMap(
- {'builder': builder, 'build_number': buildNumber, 'index': index}));
+ await documentRef.setData(
+ DocumentData.fromMap({
+ 'builder': builder,
+ 'build_number': buildNumber,
+ 'index': index
+ }),
+ SetOptions(merge: true));
info('Created build record: '
'builder: $builder, build_number: $buildNumber, index: $index');
} else if (record.data.getInt('index') != index) {
@@ -331,7 +336,6 @@
break;
}
}
-
Future<void> updateStatus(Transaction transaction) async {
final snapshot = await transaction.get(document);
final data = snapshot.data.toMap();
@@ -359,13 +363,10 @@
Future<void> storeTryChunkStatus(String builder, int buildNumber, int review,
int patchset, bool success) async {
+ await _ensureTryBuildRecord(builder, buildNumber, review, patchset);
final reference =
firestore.document('try_builds/$builder:$review:$patchset');
- final snapshot = await reference.get();
- if (!snapshot.exists || snapshot.data.getBool('completed') != null) {
- await _createTryBuildRecord(builder, buildNumber, review, patchset);
- }
Future<void> updateStatus(Transaction transaction) async {
final snapshot = await transaction.get(reference);
final data = snapshot.data.toMap();
@@ -386,23 +387,46 @@
Future<void> storeTryBuildChunkCount(String builder, int buildNumber,
int review, int patchset, int numChunks) async {
+ await _ensureTryBuildRecord(builder, buildNumber, review, patchset);
final reference =
firestore.document('try_builds/$builder:$review:$patchset');
- final snapshot = await reference.get();
- if (!snapshot.exists || snapshot.data.getInt('num_chunks') != null) {
- await _createTryBuildRecord(builder, buildNumber, review, patchset);
- }
await reference.updateData(UpdateData.fromMap({'num_chunks': numChunks}));
}
- Future<void> _createTryBuildRecord(
- String builder, int buildNumber, int review, int patchset) =>
- firestore
- .document('try_builds/$builder:$review:$patchset')
- .setData(DocumentData.fromMap({
+ Future<void> _ensureTryBuildRecord(
+ String builder, int buildNumber, int review, int patchset) async {
+ final reference =
+ firestore.document('try_builds/$builder:$review:$patchset');
+ var snapshot = await reference.get();
+ if (snapshot.exists && snapshot.data.getInt('build_number') > buildNumber) {
+ throw ArgumentError("Received chunk from previous build $buildNumber"
+ " after chunk from a later build");
+ }
+ if (snapshot.exists && snapshot.data.getInt('build_number') < buildNumber) {
+ Future<void> deleteEarlierBuild(Transaction transaction) async {
+ final snapshot = await transaction.get(reference);
+ if (snapshot.exists &&
+ snapshot.data.getInt('build_number') < buildNumber) {
+ transaction.delete(reference);
+ }
+ }
+
+ try {
+ await firestore.runTransaction(deleteEarlierBuild);
+ } finally {
+ snapshot = await reference.get();
+ }
+ }
+
+ if (!snapshot.exists) {
+ await reference.setData(
+ DocumentData.fromMap({
'builder': builder,
'build_number': buildNumber,
'review': review,
'patchset': patchset,
- }));
+ }),
+ SetOptions(merge: true));
+ }
+ }
}
diff --git a/functions/node/test/firestore_test_nodejs.dart b/functions/node/test/firestore_test_nodejs.dart
index 1ff458b..c475dc9 100644
--- a/functions/node/test/firestore_test_nodejs.dart
+++ b/functions/node/test/firestore_test_nodejs.dart
@@ -138,7 +138,7 @@
// Send first chunk of second run on the same patchset, with an approved
// failure and an unapproved failure.
await Tryjob(testReviewPath, null, firestore, null)
- .process([tryjobOtherFailingChange, tryjobFailingChange]);
+ .process([tryjob2OtherFailingChange, tryjob2FailingChange]);
final reference = fs.firestore
.document('try_builds/$testBuilder:$testReview:$testPatchset');
document = await reference.get();
@@ -149,7 +149,7 @@
expect(document.data.getInt('processed_chunks'), 1);
// Send second chunk.
await Tryjob(testReviewPath, 3, firestore, null)
- .process([tryjobExistingFailure]);
+ .process([tryjob2ExistingFailure]);
document = await reference.get();
expect(document.data.getBool('success'), isFalse);
expect(document.data.getBool('completed'), isNull);
@@ -157,7 +157,7 @@
expect(document.data.getInt('processed_chunks'), 2);
// Send third and final chunk.
await Tryjob(testReviewPath, null, firestore, null)
- .process([tryjobPassingChange]);
+ .process([tryjob2PassingChange]);
document = await reference.get();
expect(document.data.getBool('success'), isFalse);
expect(document.data.getBool('completed'), isTrue);
@@ -166,7 +166,7 @@
// Send first chunk of a third run, with only one chunk.
await Tryjob(testReviewPath, 1, firestore, null)
- .process([tryjobPassingChange]);
+ .process([tryjob3PassingChange]);
document = await reference.get();
expect(document.data.getBool('success'), isTrue);
expect(document.data.getBool('completed'), isTrue);
diff --git a/functions/node/test/test_data.dart b/functions/node/test/test_data.dart
index 9ab4e31..1f88a2a 100644
--- a/functions/node/test/test_data.dart
+++ b/functions/node/test/test_data.dart
@@ -128,6 +128,8 @@
const testBuilder = 'test_builder';
const testBuildNumber = "308";
+const tryjob2BuildNumber = "309";
+const tryjob3BuildNumber = "310";
const testConfiguration = 'test_configuration';
const testReview = 123;
const testPatchset = 3;
@@ -158,7 +160,7 @@
"changed": true
};
-final Map<String, dynamic> tryjobOtherFailingChange =
+final Map<String, dynamic> tryjob2OtherFailingChange =
Map<String, dynamic>.from(tryjobFailingChange)
..addAll({
"name": "test_suite/other_failing_test",
@@ -167,7 +169,8 @@
"expected": "Pass",
"matches": false,
"previous_result": "Pass",
- "changed": true
+ "changed": true,
+ "build_number": tryjob2BuildNumber,
});
final Map<String, dynamic> tryjobExistingFailure =
@@ -182,6 +185,18 @@
"changed": false
});
+final Map<String, dynamic> tryjob2ExistingFailure =
+ Map<String, dynamic>.from(tryjobExistingFailure)
+ ..addAll({
+ "build_number": tryjob2BuildNumber,
+ });
+
+final Map<String, dynamic> tryjob2FailingChange =
+ Map<String, dynamic>.from(tryjobFailingChange)
+ ..addAll({
+ "build_number": tryjob2BuildNumber,
+ });
+
final Map<String, dynamic> tryjobPassingChange =
Map<String, dynamic>.from(tryjobFailingChange)
..addAll({
@@ -194,6 +209,18 @@
"changed": true
});
+final Map<String, dynamic> tryjob2PassingChange =
+ Map<String, dynamic>.from(tryjobPassingChange)
+ ..addAll({
+ "build_number": tryjob2BuildNumber,
+ });
+
+final Map<String, dynamic> tryjob3PassingChange =
+ Map<String, dynamic>.from(tryjobPassingChange)
+ ..addAll({
+ "build_number": tryjob3BuildNumber,
+ });
+
String gitilesLog = '''
)]}'
{