-
Notifications
You must be signed in to change notification settings - Fork 12
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
feat: Sync skip deletes <do not merge> #2131
base: trunk
Are you sure you want to change the base?
Changes from all commits
e577b31
2187883
1a05096
5714380
e9a6356
6d706a7
0c53be2
832f142
b5bac00
38b11da
e949f00
2b9285b
8e3f568
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import 'package:at_persistence_secondary_server/src/log/commitlog/commit_entry.dart'; | ||
import 'package:at_persistence_secondary_server/src/log/commitlog/sync/sync_keys_fetch_strategy.dart'; | ||
|
||
/// Returns the commit entries which have to be synced from server to client | ||
class FetchAllKeysStrategy extends SyncKeysFetchStrategy { | ||
@override | ||
bool shouldIncludeEntryInSyncResponse( | ||
CommitEntry commitEntry, int commitId, String regex, | ||
{List<String>? enrolledNamespace}) { | ||
return commitEntry.commitId! >= commitId && | ||
super.shouldIncludeKeyInSyncResponse(commitEntry.atKey!, regex, | ||
enrolledNamespace: enrolledNamespace); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import 'package:at_persistence_secondary_server/src/log/commitlog/commit_entry.dart'; | ||
import 'package:at_persistence_secondary_server/src/log/commitlog/sync/sync_keys_fetch_strategy.dart'; | ||
|
||
/// Returns the commit entries to be returned in sync response from server to client except delete commit entries. | ||
class SkipDeleteStrategy extends SyncKeysFetchStrategy { | ||
late int skipDeletesUntil; | ||
late int latestCommitId; | ||
SkipDeleteStrategy(this.skipDeletesUntil, this.latestCommitId); | ||
@override | ||
bool shouldIncludeEntryInSyncResponse( | ||
CommitEntry commitEntry, int commitId, String regex, | ||
{List<String>? enrolledNamespace}) { | ||
// do not include delete commit entries between commitId and skipDeletesUntil, except when delete is the last commit entry | ||
if (commitEntry.operation == CommitOp.DELETE && | ||
commitEntry.commitId! <= skipDeletesUntil && | ||
commitEntry.commitId! >= commitId && | ||
commitEntry.commitId != latestCommitId) { | ||
return false; | ||
} | ||
return commitEntry.commitId! >= commitId && | ||
super.shouldIncludeKeyInSyncResponse(commitEntry.atKey!, regex, | ||
enrolledNamespace: enrolledNamespace); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import 'package:at_commons/at_commons.dart'; | ||
import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; | ||
import 'package:at_utils/at_utils.dart'; | ||
|
||
abstract class SyncKeysFetchStrategy { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we call it NamespaceHonoringKeyFetchStrategy? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honoring namespace is one of the checks done. regex match and skipping delete commits are the other two checks. In the future if we have any other checks not related to namespace we may have to rename this class |
||
final _logger = AtSignLogger('SyncKeysFetchStrategy'); | ||
|
||
/// Returns true if the commit entry should be included in sync response, false otherwise | ||
bool shouldIncludeEntryInSyncResponse( | ||
CommitEntry commitEntry, int commitId, String regex, | ||
{List<String>? enrolledNamespace}); | ||
|
||
/// if enrolledNamespace is passed, key namespace should be in enrolledNamespace list and | ||
/// atKey should match regex or should be a special key that is always included in sync. | ||
bool shouldIncludeKeyInSyncResponse(String atKey, String regex, | ||
{List<String>? enrolledNamespace}) { | ||
return isNamespaceAuthorised(atKey, enrolledNamespace) && | ||
(keyMatchesRegex(atKey, regex) || alwaysIncludeInSync(atKey)); | ||
} | ||
|
||
/// Returns true if atKey namespace is empty or null/ enrolledNamespace is empty or null | ||
/// if enrolledNamespace contains atKey namespace, return true. false otherwise | ||
bool isNamespaceAuthorised( | ||
String atKeyAsString, List<String>? enrolledNamespace) { | ||
// This is work-around for : https://github.com/atsign-foundation/at_server/issues/1570 | ||
if (atKeyAsString.toLowerCase() == 'configkey') { | ||
return true; | ||
} | ||
late AtKey atKey; | ||
try { | ||
atKey = AtKey.fromString(atKeyAsString); | ||
} on InvalidSyntaxException catch (_) { | ||
_logger.warning( | ||
'_isNamespaceAuthorized found an invalid key "$atKeyAsString" in the commit log. Returning false'); | ||
return false; | ||
} | ||
String? keyNamespace = atKey.namespace; | ||
// If enrolledNamespace is null or keyNamespace is null, fallback to | ||
// existing behaviour - the key is authorized for the client to receive. So return true. | ||
if (enrolledNamespace == null || | ||
enrolledNamespace.isEmpty || | ||
(keyNamespace == null || keyNamespace.isEmpty)) { | ||
return true; | ||
} | ||
if (enrolledNamespace.contains('*') || | ||
enrolledNamespace.contains(keyNamespace)) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
/// Returns true if atKey matches regex. false otherwise | ||
bool keyMatchesRegex(String atKey, String regex) { | ||
return RegExp(regex).hasMatch(atKey); | ||
} | ||
|
||
/// match keys which have to included in sync irrespective of whether regex matches | ||
/// e.g @bob:shared_key@alice, shared_key.bob@alice, public:publickey@alice, | ||
/// public:phone@alice (public key without namespace) | ||
bool alwaysIncludeInSync(String atKey) { | ||
return (atKey.contains(AtConstants.atEncryptionSharedKey) && | ||
RegexUtil.keyType(atKey, false) == KeyType.reservedKey) || | ||
atKey.startsWith(AtConstants.atEncryptionPublicKey) || | ||
(atKey.startsWith('public:') && !atKey.contains('.')); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -840,6 +840,92 @@ void main() async { | |
expect(commitEntriesMap.containsKey('public:phone.wavi@alice'), false); | ||
expect(commitEntriesMap.containsKey('public:location@alice'), true); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add another test which both uses a regex and sets skipDeletes: true There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
test( | ||
'A test to verify delete commit entries are NOT returned when skipDeletesUntil is set', | ||
() async { | ||
var commitLogInstance = | ||
await (AtCommitLogManagerImpl.getInstance().getCommitLog('@alice')); | ||
var commitLogKeystore = commitLogInstance!.commitLogKeyStore; | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_1@alice', CommitOp.UPDATE, DateTime.now())); | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_2@alice', CommitOp.DELETE, DateTime.now())); | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_3@alice', CommitOp.DELETE, DateTime.now())); | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_4@alice', CommitOp.UPDATE, DateTime.now())); | ||
Iterator<MapEntry<String, CommitEntry>>? changes = commitLogInstance | ||
.commitLogKeyStore | ||
.getEntries(-1, skipDeletesUntil: 25); | ||
Map<String?, CommitEntry> commitEntriesMap = {}; | ||
while (changes.moveNext()) { | ||
var commitEntry = changes.current.value; | ||
commitEntriesMap[commitEntry.atKey] = commitEntry; | ||
} | ||
expect(commitEntriesMap.containsKey('test_key_true_1@alice'), true); | ||
expect(commitEntriesMap.containsKey('test_key_true_2@alice'), false); | ||
expect(commitEntriesMap.containsKey('test_key_true_3@alice'), false); | ||
expect(commitEntriesMap.containsKey('test_key_true_4@alice'), true); | ||
}); | ||
test( | ||
'A test to verify correct commit entries are returned when skipDeletesUntil is set and regex is passed', | ||
() async { | ||
var commitLogInstance = | ||
await (AtCommitLogManagerImpl.getInstance().getCommitLog('@alice')); | ||
var commitLogKeystore = commitLogInstance!.commitLogKeyStore; | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_1.wavi@alice', CommitOp.UPDATE, DateTime.now())); | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_2.buzz@alice', CommitOp.DELETE, DateTime.now())); | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_3.wavi@alice', CommitOp.DELETE, DateTime.now())); | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_4.buzz@alice', CommitOp.UPDATE, DateTime.now())); | ||
Iterator<MapEntry<String, CommitEntry>>? changes = commitLogInstance | ||
.commitLogKeyStore | ||
.getEntries(-1, skipDeletesUntil: 25, regex: '.buzz'); | ||
Map<String?, CommitEntry> commitEntriesMap = {}; | ||
while (changes.moveNext()) { | ||
var commitEntry = changes.current.value; | ||
commitEntriesMap[commitEntry.atKey] = commitEntry; | ||
} | ||
expect( | ||
commitEntriesMap.containsKey('test_key_true_1.wavi@alice'), false); | ||
expect( | ||
commitEntriesMap.containsKey('test_key_true_2.buzz@alice'), false); | ||
expect( | ||
commitEntriesMap.containsKey('test_key_true_3.wavi@alice'), false); | ||
expect( | ||
commitEntriesMap.containsKey('test_key_true_4.buzz@alice'), true); | ||
}); | ||
test( | ||
'A test to verify last delete commit entry is returned when its commitId is equal to latest commitId', | ||
() async { | ||
var commitLogInstance = | ||
await (AtCommitLogManagerImpl.getInstance().getCommitLog('@alice')); | ||
var commitLogKeystore = commitLogInstance!.commitLogKeyStore; | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_1@alice', CommitOp.UPDATE, DateTime.now())); | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_2@alice', CommitOp.DELETE, DateTime.now())); | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_3@alice', CommitOp.DELETE, DateTime.now())); | ||
await commitLogKeystore.add(CommitEntry( | ||
'test_key_true_4@alice', CommitOp.DELETE, DateTime.now())); | ||
int? latestCommitId = commitLogInstance.lastCommittedSequenceNumber(); | ||
Iterator<MapEntry<String, CommitEntry>>? changes = commitLogInstance | ||
.commitLogKeyStore | ||
.getEntries(-1, skipDeletesUntil: latestCommitId); | ||
Map<String?, CommitEntry> commitEntriesMap = {}; | ||
while (changes.moveNext()) { | ||
var commitEntry = changes.current.value; | ||
commitEntriesMap[commitEntry.atKey] = commitEntry; | ||
} | ||
expect(commitEntriesMap.containsKey('test_key_true_1@alice'), true); | ||
expect(commitEntriesMap.containsKey('test_key_true_2@alice'), false); | ||
expect(commitEntriesMap.containsKey('test_key_true_3@alice'), false); | ||
expect(commitEntriesMap.containsKey('test_key_true_4@alice'), true); | ||
}); | ||
}); | ||
tearDown(() async => await tearDownFunc()); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dont we need call to super.shouldIncludeEntryInSyncResponse()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i kept the method in base class abstract