Skip to content

Commit 71e71ed

Browse files
committed
Change default to no retry for write operations
1 parent aab707e commit 71e71ed

File tree

6 files changed

+168
-11
lines changed

6 files changed

+168
-11
lines changed

packages/dart/lib/parse_server_sdk.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class Parse {
120120
ParseUserConstructor? parseUserConstructor,
121121
ParseFileConstructor? parseFileConstructor,
122122
List<int>? restRetryIntervals,
123+
List<int>? restRetryIntervalsForWrites,
123124
List<int>? liveListRetryIntervals,
124125
ParseConnectivityProvider? connectivityProvider,
125126
String? fileDirectory,
@@ -147,6 +148,7 @@ class Parse {
147148
parseUserConstructor: parseUserConstructor,
148149
parseFileConstructor: parseFileConstructor,
149150
restRetryIntervals: restRetryIntervals,
151+
restRetryIntervalsForWrites: restRetryIntervalsForWrites,
150152
liveListRetryIntervals: liveListRetryIntervals,
151153
connectivityProvider: connectivityProvider,
152154
fileDirectory: fileDirectory,

packages/dart/lib/src/data/parse_core_data.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ParseCoreData {
3333
ParseUserConstructor? parseUserConstructor,
3434
ParseFileConstructor? parseFileConstructor,
3535
List<int>? restRetryIntervals,
36+
List<int>? restRetryIntervalsForWrites,
3637
List<int>? liveListRetryIntervals,
3738
ParseConnectivityProvider? connectivityProvider,
3839
String? fileDirectory,
@@ -55,6 +56,8 @@ class ParseCoreData {
5556
_instance.securityContext = securityContext;
5657
_instance.restRetryIntervals =
5758
restRetryIntervals ?? <int>[0, 250, 500, 1000, 2000];
59+
_instance.restRetryIntervalsForWrites =
60+
restRetryIntervalsForWrites ?? <int>[];
5861
_instance.liveListRetryIntervals =
5962
liveListRetryIntervals ??
6063
(parseIsWeb
@@ -93,6 +96,7 @@ class ParseCoreData {
9396
late CoreStore storage;
9497
late ParseSubClassHandler _subClassHandler;
9598
late List<int> restRetryIntervals;
99+
late List<int> restRetryIntervalsForWrites;
96100
late List<int> liveListRetryIntervals;
97101
ParseConnectivityProvider? connectivityProvider;
98102
String? fileDirectory;

packages/dart/lib/src/network/parse_dio_client.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ class ParseDioClient extends ParseClient {
108108
ParseNetworkOptions? options,
109109
}) async {
110110
return executeWithRetry(
111+
isWriteOperation: true,
111112
operation: () async {
112113
try {
113114
final dio.Response<String> dioResponse = await _client.put<String>(
@@ -137,6 +138,7 @@ class ParseDioClient extends ParseClient {
137138
ParseNetworkOptions? options,
138139
}) async {
139140
return executeWithRetry(
141+
isWriteOperation: true,
140142
operation: () async {
141143
try {
142144
final dio.Response<String> dioResponse = await _client.post<String>(
@@ -168,6 +170,7 @@ class ParseDioClient extends ParseClient {
168170
dynamic cancelToken,
169171
}) async {
170172
return executeWithRetry(
173+
isWriteOperation: true,
171174
operation: () async {
172175
try {
173176
final dio.Response<String> dioResponse = await _client.post<String>(

packages/dart/lib/src/network/parse_http_client.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class ParseHTTPClient extends ParseClient {
103103
ParseNetworkOptions? options,
104104
}) async {
105105
return executeWithRetry(
106+
isWriteOperation: true,
106107
operation: () async {
107108
try {
108109
final http.Response response = await _client.put(
@@ -131,6 +132,7 @@ class ParseHTTPClient extends ParseClient {
131132
ParseNetworkOptions? options,
132133
}) async {
133134
return executeWithRetry(
135+
isWriteOperation: true,
134136
operation: () async {
135137
try {
136138
final http.Response response = await _client.post(
@@ -161,6 +163,7 @@ class ParseHTTPClient extends ParseClient {
161163
dynamic cancelToken,
162164
}) async {
163165
return executeWithRetry(
166+
isWriteOperation: true,
164167
operation: () async {
165168
try {
166169
final http.Response response = await _client.post(

packages/dart/lib/src/network/parse_network_retry.dart

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,33 @@ part of '../../parse_server_sdk.dart';
2424
///
2525
/// Important Note on Non-Idempotent Methods (POST/PUT):
2626
///
27-
/// This retry mechanism is applied to ALL HTTP methods including POST and PUT.
28-
/// While GET and DELETE are generally safe to retry, POST and PUT operations
29-
/// may cause duplicate operations if the original request succeeded but the
30-
/// response was lost or corrupted.
31-
///
3227
/// **Parse Server does not provide automatic optimistic locking or built-in
33-
/// idempotency guarantees for POST/PUT operations.** Retrying these methods
34-
/// can result in duplicate data creation or unintended state changes.
28+
/// idempotency guarantees for POST/PUT operations.** To prevent duplicate
29+
/// data creation or unintended state changes, this SDK defaults to **no retries**
30+
/// for write operations (POST/PUT/postBytes).
31+
///
32+
/// Default Behavior:
33+
/// - **Write operations (POST/PUT)**: No retries (`restRetryIntervalsForWrites = []`)
34+
/// - **Read operations (GET)**: Retries enabled (`restRetryIntervals = [0, 250, 500, 1000, 2000]`)
35+
/// - **DELETE operations**: Retries enabled (generally safe to retry)
3536
///
36-
/// To mitigate retry risks for critical operations:
37+
/// If you need to enable retries for write operations, configure
38+
/// `restRetryIntervalsForWrites` during initialization. Consider these mitigations:
3739
/// - Implement application-level idempotency keys or version tracking
38-
/// - Disable retries for create/update operations by setting
39-
/// `ParseCoreData().restRetryIntervals = []` before critical calls
4040
/// - Use Parse's experimental `X-Parse-Request-Id` header (if available)
4141
/// with explicit duplicate detection in your application logic
42+
/// - Use conservative retry intervals (e.g., `[1000, 2000]`) to allow time
43+
/// for server-side processing before retrying
44+
///
45+
/// Example:
46+
/// ```dart
47+
/// await Parse().initialize(
48+
/// 'appId',
49+
/// 'serverUrl',
50+
/// // Enable retries for writes (use with caution)
51+
/// restRetryIntervalsForWrites: [1000, 2000],
52+
/// );
53+
/// ```
4254
///
4355
/// Note: Retries only occur on network-level failures (status -1), not on
4456
/// successful operations that return Parse error codes
@@ -58,6 +70,9 @@ part of '../../parse_server_sdk.dart';
5870
/// Parameters:
5971
///
6072
/// - [operation]: The network operation to execute and potentially retry
73+
/// - [isWriteOperation]: Whether this is a write operation (POST/PUT).
74+
/// Defaults to `false`. When `true`, uses [ParseCoreData.restRetryIntervalsForWrites]
75+
/// which defaults to no retries to prevent duplicate creates/updates.
6176
/// - [debug]: Whether to log retry attempts (defaults to [ParseCoreData.debug])
6277
///
6378
/// Returns:
@@ -66,9 +81,12 @@ part of '../../parse_server_sdk.dart';
6681
/// after all retry attempts are exhausted.
6782
Future<T> executeWithRetry<T extends ParseNetworkResponse>({
6883
required Future<T> Function() operation,
84+
bool isWriteOperation = false,
6985
bool? debug,
7086
}) async {
71-
final List<int> retryIntervals = ParseCoreData().restRetryIntervals;
87+
final List<int> retryIntervals = isWriteOperation
88+
? ParseCoreData().restRetryIntervalsForWrites
89+
: ParseCoreData().restRetryIntervals;
7290
final bool debugEnabled = debug ?? ParseCoreData().debug;
7391

7492
// Enforce maximum retry limit to prevent excessive attempts

packages/dart/test/src/network/parse_network_retry_test.dart

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,4 +510,131 @@ void main() {
510510
expect(result.data, contains('"error"'));
511511
});
512512
});
513+
514+
group('Write Operations (POST/PUT) Retry Behavior', () {
515+
test(
516+
'should not retry write operations by default (restRetryIntervalsForWrites is empty)',
517+
() async {
518+
int callCount = 0;
519+
final result = await executeWithRetry(
520+
isWriteOperation: true,
521+
operation: () async {
522+
callCount++;
523+
return ParseNetworkResponse(
524+
data: '{"code":-1,"error":"NetworkError"}',
525+
statusCode: -1,
526+
);
527+
},
528+
);
529+
530+
// Should only be called once (no retries)
531+
expect(callCount, 1);
532+
expect(result.statusCode, -1);
533+
},
534+
);
535+
536+
test(
537+
'should use restRetryIntervalsForWrites when configured for write operations',
538+
() async {
539+
int callCount = 0;
540+
final oldWriteIntervals = ParseCoreData().restRetryIntervalsForWrites;
541+
ParseCoreData().restRetryIntervalsForWrites = [0, 10]; // 2 retries
542+
543+
final result = await executeWithRetry(
544+
isWriteOperation: true,
545+
operation: () async {
546+
callCount++;
547+
return ParseNetworkResponse(
548+
data: '{"code":-1,"error":"NetworkError"}',
549+
statusCode: -1,
550+
);
551+
},
552+
);
553+
554+
// Should be called: initial + 2 retries = 3 times
555+
expect(callCount, 3);
556+
expect(result.statusCode, -1);
557+
558+
ParseCoreData().restRetryIntervalsForWrites = oldWriteIntervals;
559+
},
560+
);
561+
562+
test(
563+
'should use restRetryIntervals for read operations (isWriteOperation=false)',
564+
() async {
565+
int callCount = 0;
566+
final oldIntervals = ParseCoreData().restRetryIntervals;
567+
ParseCoreData().restRetryIntervals = [0, 10]; // 2 retries
568+
569+
final result = await executeWithRetry(
570+
isWriteOperation: false,
571+
operation: () async {
572+
callCount++;
573+
return ParseNetworkResponse(
574+
data: '{"code":-1,"error":"NetworkError"}',
575+
statusCode: -1,
576+
);
577+
},
578+
);
579+
580+
// Should be called: initial + 2 retries = 3 times
581+
expect(callCount, 3);
582+
expect(result.statusCode, -1);
583+
584+
ParseCoreData().restRetryIntervals = oldIntervals;
585+
},
586+
);
587+
588+
test(
589+
'write operations succeed immediately on success without retries',
590+
() async {
591+
int callCount = 0;
592+
final result = await executeWithRetry(
593+
isWriteOperation: true,
594+
operation: () async {
595+
callCount++;
596+
return ParseNetworkResponse(
597+
data: '{"objectId":"abc123"}',
598+
statusCode: 201,
599+
);
600+
},
601+
);
602+
603+
expect(callCount, 1);
604+
expect(result.statusCode, 201);
605+
},
606+
);
607+
608+
test(
609+
'write operations can be configured with custom retry intervals',
610+
() async {
611+
int callCount = 0;
612+
final oldWriteIntervals = ParseCoreData().restRetryIntervalsForWrites;
613+
ParseCoreData().restRetryIntervalsForWrites = [5, 10, 15];
614+
615+
final result = await executeWithRetry(
616+
isWriteOperation: true,
617+
operation: () async {
618+
callCount++;
619+
if (callCount < 3) {
620+
return ParseNetworkResponse(
621+
data: '{"code":-1,"error":"NetworkError"}',
622+
statusCode: -1,
623+
);
624+
}
625+
return ParseNetworkResponse(
626+
data: '{"objectId":"abc123"}',
627+
statusCode: 201,
628+
);
629+
},
630+
);
631+
632+
// Should succeed on third attempt
633+
expect(callCount, 3);
634+
expect(result.statusCode, 201);
635+
636+
ParseCoreData().restRetryIntervalsForWrites = oldWriteIntervals;
637+
},
638+
);
639+
});
513640
}

0 commit comments

Comments
 (0)