Skip to content

Commit

Permalink
Support retrying Datastore operations. (dart-archive/gcloud#168)
Browse files Browse the repository at this point in the history
* Support retrying Datastore operations.

* Expose only maxAttempts

* add more errors
  • Loading branch information
isoos authored Jul 21, 2023
1 parent bdd4244 commit 0b7c4bb
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 4 deletions.
3 changes: 2 additions & 1 deletion pkgs/gcloud/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 0.8.10-wip
## 0.8.10

- Widen the SDK constraint to support Dart 3.0
- Support retrying Datastore operations.

## 0.8.9

Expand Down
18 changes: 18 additions & 0 deletions pkgs/gcloud/lib/datastore.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ library;
import 'dart:async';

import 'package:http/http.dart' as http;
import 'package:retry/retry.dart';

import 'common.dart' show Page;
import 'service_scope.dart' as ss;
import 'src/datastore_impl.dart' show DatastoreImpl;
import 'src/retry_datastore_impl.dart';

const Symbol _datastoreKey = #gcloud.datastore;

Expand Down Expand Up @@ -391,6 +393,22 @@ abstract class Datastore {
return DatastoreImpl(client, project);
}

/// Retry Datastore operations where the issue seems to be transient.
///
/// The [delegate] is the configured [Datastore] implementation that will be
/// used.
///
/// The operations will be retried at maximum of [maxAttempts].
factory Datastore.withRetry(
Datastore delegate, {
int? maxAttempts,
}) {
return RetryDatastoreImpl(
delegate,
RetryOptions(maxAttempts: maxAttempts ?? 3),
);
}

/// Allocate integer IDs for the partially populated [keys] given as argument.
///
/// The returned [Key]s will be fully populated with the allocated IDs.
Expand Down
159 changes: 159 additions & 0 deletions pkgs/gcloud/lib/src/retry_datastore_impl.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:retry/retry.dart';

import '../common.dart';
import '../datastore.dart' as datastore;

/// Datastore implementation which retries most operations
class RetryDatastoreImpl implements datastore.Datastore {
final datastore.Datastore _delegate;
final RetryOptions _retryOptions;

RetryDatastoreImpl(this._delegate, this._retryOptions);

@override
Future<List<datastore.Key>> allocateIds(List<datastore.Key> keys) async {
return await _retryOptions.retry(
() => _delegate.allocateIds(keys),
retryIf: _retryIf,
);
}

@override
Future<datastore.Transaction> beginTransaction({
bool crossEntityGroup = false,
}) async {
return await _retryOptions.retry(
() => _delegate.beginTransaction(crossEntityGroup: crossEntityGroup),
retryIf: _retryIf,
);
}

@override
Future<datastore.CommitResult> commit({
List<datastore.Entity> inserts = const [],
List<datastore.Entity> autoIdInserts = const [],
List<datastore.Key> deletes = const [],
datastore.Transaction? transaction,
}) async {
Future<datastore.CommitResult> fn() async {
if (transaction == null) {
return await _delegate.commit(
inserts: inserts,
autoIdInserts: autoIdInserts,
deletes: deletes,
);
} else {
return await _delegate.commit(
inserts: inserts,
autoIdInserts: autoIdInserts,
deletes: deletes,
transaction: transaction,
);
}
}

final shouldNotRetry = autoIdInserts.isNotEmpty && transaction == null;
if (shouldNotRetry) {
return await fn();
} else {
return await _retryOptions.retry(fn, retryIf: _retryIf);
}
}

@override
Future<List<datastore.Entity?>> lookup(
List<datastore.Key> keys, {
datastore.Transaction? transaction,
}) async {
return await _retryOptions.retry(
() async {
if (transaction == null) {
return await _delegate.lookup(keys);
} else {
return await _delegate.lookup(keys, transaction: transaction);
}
},
retryIf: _retryIf,
);
}

@override
Future<Page<datastore.Entity>> query(
datastore.Query query, {
datastore.Partition? partition,
datastore.Transaction? transaction,
}) async {
Future<Page<datastore.Entity>> fn() async {
if (partition != null && transaction != null) {
return await _delegate.query(
query,
partition: partition,
transaction: transaction,
);
} else if (partition != null) {
return await _delegate.query(query, partition: partition);
} else if (transaction != null) {
return await _delegate.query(
query,
transaction: transaction,
);
} else {
return await _delegate.query(query);
}
}

return await _retryOptions.retry(
() async => _RetryPage(await fn(), _retryOptions),
retryIf: _retryIf,
);
}

@override
Future rollback(datastore.Transaction transaction) async {
return await _retryOptions.retry(
() => _delegate.rollback(transaction),
retryIf: _retryIf,
);
}
}

class _RetryPage<K> implements Page<K> {
final Page<K> _delegate;
final RetryOptions _retryOptions;

_RetryPage(this._delegate, this._retryOptions);

@override
bool get isLast => _delegate.isLast;

@override
List<K> get items => _delegate.items;

@override
Future<Page<K>> next({int? pageSize}) async {
return await _retryOptions.retry(
() async {
if (pageSize == null) {
return await _delegate.next();
} else {
return await _delegate.next(pageSize: pageSize);
}
},
retryIf: _retryIf,
);
}
}

bool _retryIf(Exception e) {
if (e is datastore.TransactionAbortedError ||
e is datastore.NeedIndexError ||
e is datastore.QuotaExceededError ||
e is datastore.PermissionDeniedError) {
return false;
}
return true;
}
3 changes: 2 additions & 1 deletion pkgs/gcloud/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: gcloud
version: 0.8.10-wip
version: 0.8.10
description: >-
High level idiomatic Dart API for Google Cloud Storage, Pub-Sub and Datastore.
repository: https://github.com/dart-lang/gcloud
Expand All @@ -16,6 +16,7 @@ dependencies:
googleapis: '>=3.0.0 <12.0.0'
http: '>=0.13.5 <2.0.0'
meta: ^1.3.0
retry: ^3.1.1

dev_dependencies:
dart_flutter_team_lints: ^1.0.0
Expand Down
6 changes: 4 additions & 2 deletions pkgs/gcloud/test/db_all_e2e_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ library gcloud.test.db_all_test;
import 'dart:async';
import 'dart:io';

import 'package:gcloud/datastore.dart';
import 'package:gcloud/db.dart' as db;
import 'package:gcloud/src/datastore_impl.dart' as datastore_impl;
import 'package:http/http.dart';
Expand All @@ -25,12 +26,13 @@ Future main() async {
var now = DateTime.now().millisecondsSinceEpoch;
var namespace = '${Platform.operatingSystem}$now';

late datastore_impl.DatastoreImpl datastore;
late Datastore datastore;
late db.DatastoreDB datastoreDB;
Client? client;

await withAuthClient(scopes, (String project, httpClient) async {
datastore = datastore_impl.DatastoreImpl(httpClient, project);
datastore =
Datastore.withRetry(datastore_impl.DatastoreImpl(httpClient, project));
datastoreDB = db.DatastoreDB(datastore);
client = httpClient;
});
Expand Down

0 comments on commit 0b7c4bb

Please sign in to comment.