Skip to content

fix(secure_storage): handle parallel read/write/delete operations on Windows #5194

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import 'dart:async';
import 'dart:convert';
import 'dart:io';

Expand All @@ -18,7 +19,7 @@ import 'package:path/path.dart' as pkg_path;
// without bringing in flutter as a dependency to the tests.
class FileKeyValueStore {
/// {@macro amplify_secure_storage_dart.file_key_value_store}
const FileKeyValueStore({
FileKeyValueStore({
required this.path,
required this.fileName,
this.fs = const local_file.LocalFileSystem(),
Expand All @@ -32,6 +33,8 @@ class FileKeyValueStore {
/// The file will be created if it does not yet exist.
final String fileName;

final TaskScheduler _scheduler = TaskScheduler();

@visibleForTesting
final pkg_file.FileSystem fs;

Expand All @@ -47,9 +50,11 @@ class FileKeyValueStore {
required String key,
required Object value,
}) async {
final data = await readAll();
data[key] = value;
return writeAll(data);
return _scheduler.schedule(() async {
final data = await readAll();
data[key] = value;
return writeAll(data);
});
}

/// Overwrites the existing data.
Expand All @@ -67,25 +72,31 @@ class FileKeyValueStore {
Future<Object?> readKey({
required String key,
}) async {
final data = await readAll();
return data[key];
return _scheduler.schedule(() async {
final data = await readAll();
return data[key];
});
}

/// Removes a single key from storage.
Future<void> removeKey({
required String key,
}) async {
final data = await readAll();
data.remove(key);
await writeAll(data);
return _scheduler.schedule(() async {
final data = await readAll();
data.remove(key);
await writeAll(data);
});
}

/// Returns true if the key exists in storage
Future<bool> containsKey({
required String key,
}) async {
final data = await readAll();
return data.containsKey(key);
return _scheduler.schedule(() async {
final data = await readAll();
return data.containsKey(key);
});
}

/// Reads all the key-value pairs from storage.
Expand All @@ -102,3 +113,37 @@ class FileKeyValueStore {
return <String, Object>{};
}
}

/// A class for processing async tasks one at a time in the order that they are
/// scheduled.
class TaskScheduler {
final _taskQueue = <Task<dynamic>>[];
bool _isProcessing = false;
Future<T> schedule<T>(Future<T> Function() task) {
final completer = Completer<T>();
_taskQueue.add(Task(task, completer));
_processTasks();
return completer.future;
}

Future<void> _processTasks() async {
if (_isProcessing) return;
_isProcessing = true;
while (_taskQueue.isNotEmpty) {
final currentTask = _taskQueue.removeAt(0);
try {
final result = await currentTask.task();
currentTask.completer.complete(result);
} on Object catch (e, stackTrace) {
currentTask.completer.completeError(e, stackTrace);
}
}
_isProcessing = false;
}
}

class Task<T> {
Task(this.task, this.completer);
final Future<T> Function() task;
final Completer<T> completer;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
@TestOn('vm')

import 'package:amplify_secure_storage_dart/src/utils/file_key_value_store.dart';
import 'package:file/memory.dart';
import 'package:test/test.dart';

void main() {
Expand All @@ -14,10 +13,13 @@ void main() {
storage = FileKeyValueStore(
path: 'path',
fileName: 'file',
fs: MemoryFileSystem(),
);
});

tearDown(() async {
await storage.file.delete();
});

test('readKey & writeKey', () async {
// assert initial state is null
final value1 = await storage.readKey(key: 'key');
Expand Down Expand Up @@ -81,5 +83,30 @@ void main() {
final includesKey2 = await storage.containsKey(key: 'key2');
expect(includesKey2, isFalse);
});

test('parallel writes should occur in the order they are called', () async {
final items = List.generate(1000, ((i) => i));
final futures = items.map(
(i) async => storage.writeKey(key: 'key', value: i),
);
await Future.wait(futures);
final value = await storage.readKey(key: 'key');
expect(value, items.last);
});

// Reference: https://github.com/aws-amplify/amplify-flutter/issues/5190
test('parallel write/remove operations should not corrupt the file',
() async {
final items = List.generate(1000, ((i) => i));
final futures = items.map(
(i) async {
if (i % 5 == 1) {
await storage.removeKey(key: 'key_${i - 1}');
}
return storage.writeKey(key: 'key_$i', value: 'value_$i');
},
);
await Future.wait(futures);
});
});
}
Loading