-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Process safe deleteRealm and assetFile #5417
Conversation
- Use SharedRealm::call_with_lock() to make sure deleteRealm and assetFile calls are process safe. - Also those calls will throw if the Realm instance on the sync client thread is still opened. - Disallow deleteRealm on a synced Realm file. See #5416 . - Manually delete SharedRealm pointer if the SharedRealm was created for callbacks, eg.: initialization_data_callback and migration_callback.
CHANGELOG.md
Outdated
|
||
## Bug Fixes | ||
|
||
* Assigning a managed object's own list to itself would accidentally clear it (#5395). | ||
* Don't try to acquire `ApplicationContext` if not available in `Realm.init(Context)` (#5389). | ||
* Fix some potential database corruption caused by deleting realm file while a Realm instance are still opened on the other process or sync client thread. |
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.
in another process?
CHANGELOG.md
Outdated
|
||
## Bug Fixes | ||
|
||
* Assigning a managed object's own list to itself would accidentally clear it (#5395). | ||
* Don't try to acquire `ApplicationContext` if not available in `Realm.init(Context)` (#5389). | ||
* Fix some potential database corruption caused by deleting realm file while a Realm instance are still opened on the other process or sync client thread. |
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.
is still open in another process
@@ -74,6 +72,18 @@ public void tearDown() { | |||
} | |||
|
|||
private SharedRealm getSharedRealm() { | |||
RealmConfiguration config; | |||
config = configFactory.createConfiguration(); |
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.
why not RealmConfiguration config = configFactory.createConfiguration();
?
|
||
private SharedRealm getSharedRealmForLooper() { | ||
RealmConfiguration config; | ||
config = looperThread.createConfiguration(); |
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.
ditto
@@ -1657,14 +1657,24 @@ public static void migrateRealm(RealmConfiguration configuration, @Nullable Real | |||
} | |||
|
|||
/** | |||
* Deletes the Realm file specified by the given {@link RealmConfiguration} from the filesystem. | |||
* Deletes the Realm file along with the related temporary files specified by the given {@link RealmConfiguration} | |||
* from the filesystem. Temporary file with ".lock" extension won't be deleted. |
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.
is Core responsible for deleting the lock file? and when?
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.
no, lock file should never be deleted. There is no reliable way to delete it for multi processes case.
if (!Util.isEmptyString(syncServerCertificateAssetName)) { | ||
String syncServerCertificateFilePath = ObjectServerFacade.getFacade(configuration.isSyncConfiguration()).getSyncServerCertificateFilePath(configuration); | ||
// Copy Sync Server certificate path if available | ||
if (!Util.isEmptyString(syncServerCertificateAssetName)) { |
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.
use a final boolean
in https://github.com/realm/realm-java/pull/5417/files#diff-79e6ca3beee0a83dd76772285b606694R504 to the redundant check again
* @param runnable to run with lock. | ||
* @return true if the lock can be acquired and the {@code runnable} has been executed. | ||
*/ | ||
public static boolean callWithLock(RealmConfiguration configuration, Runnable runnable) { |
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.
What happens in case of an exception (within the Runnable) is the lock released? How are we reporting unchecked exception?
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.
yes, the lock will be released.
I planned to make a test case for this but forgot it finally ... will add a test case.
*/ | ||
private SharedRealm(long nativeSharedRealmPtr, OsRealmConfig osRealmConfig) { | ||
this.nativePtr = nativeSharedRealmPtr; | ||
this.osRealmConfig = osRealmConfig; | ||
this.schemaInfo = new OsSchemaInfo(nativeGetSchemaInfo(nativePtr), this); | ||
this.context = osRealmConfig.getContext(); | ||
this.context.addReference(this); |
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.
does it mean we shouldn't this.context.addReference(this);
when isPhantomRef
is true
?
what's the purpose of keeping the NativeObject
interface if we don't add the reference(addReference
)
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.
See https://github.com/realm/realm-java/pull/5417/files#diff-6e1f09495610b341d4f834a4e13002ccR199
In normal case it will still be managed by phantom reference.
This constructor is a special case and only handled in this file.
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.
*isPhantomRef
is no longer used? ( you removed the delete
method) ...?
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 removed both in the yesterday's change.
So the pointer itself will be managed like a normal SharedRealm
pointer. So there is no chance for users to meet a invalid pointer anymore. (before yeterday's change, there was still a problem.)
See commit ed305d2
realmDeleted = realmDeleted && file.delete(); | ||
boolean deleteResult = file.delete(); | ||
if (!deleteResult) { | ||
RealmLog.warn( String.format(Locale.ENGLISH,"Realm temporary file at %s cannot deleted", |
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.
cannot be deleted
realmDeleted = realmFile.delete(); | ||
if (!realmDeleted) { | ||
RealmLog.warn( | ||
String.format(Locale.ENGLISH,"Realm file at %s cannot deleted", realmFile.getAbsolutePath())); |
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.
cannot be deleted
|
||
// 2. Local state should now be completely reset. Open the Realm again with a new configuration which should | ||
// download the uploaded changes. | ||
user = SyncUser.login(SyncCredentials.usernamePassword(username, password), Constants.AUTH_URL); | ||
SyncConfiguration config = configurationFactory.createSyncConfigurationBuilder(user, Constants.USER_REALM_SECURE) | ||
.name("newRealm") |
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.
These tests are disabled for now, https://github.com/realm/ros/issues/512 any reason to add the name?
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.
because of we sadly don't have a reliable way to know when the sync client thread will close the Realm instance.
https://github.com/realm/realm-java/pull/5417/files#diff-c4f8c4a7fed4b6347565a6e587f5da61L68
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.
Apart from minor comments 👍
CHANGELOG.md
Outdated
@@ -2,15 +2,19 @@ | |||
|
|||
## Breaking Changes | |||
|
|||
* `Realm.deleteRealm()` doesn't support synced Realm for now. See issue #5416. |
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.
Realm -> Realms
CHANGELOG.md
Outdated
|
||
## Bug Fixes | ||
|
||
* Assigning a managed object's own list to itself would accidentally clear it (#5395). | ||
* Don't try to acquire `ApplicationContext` if not available in `Realm.init(Context)` (#5389). | ||
* Fix some potential database corruption caused by deleting realm file while a Realm instance are still opened in another process or the sync client thread. |
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.
deleting realm file -> deleting the Realm file
@@ -2302,7 +2302,11 @@ public void run() { | |||
assertTrue(Realm.deleteRealm(configuration)); | |||
|
|||
// Directory should be empty now. |
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.
That note doesn't seem correct given that the length is 0. I assume it is the .lock
file. Clarify this or just move the comment below up instead of this one?
bool result = SharedGroup::call_with_lock(realm_path, [&](std::string path) { | ||
REALM_ASSERT_RELEASE_EX(realm_path.compare(path) == 0, realm_path.c_str(), path.c_str()); | ||
env->CallVoidMethod(j_runnable, run_method); | ||
exception_thrown = env->ExceptionCheck(); |
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.
This callback is made on the same thread right, so using the env
should be safe here?
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.
yes, it is a synchronized callback. It will return before the outer function returns.
* | ||
* @param configuration a {@link RealmConfiguration}. | ||
* @return {@code false} if a file could not be deleted. The failing file will be logged. | ||
* @return {@code false} if the realm file could not be deleted. Temporary files deletion failure won't impact on |
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.
remove on
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.
realm -> Realm
@@ -71,6 +73,19 @@ public static boolean deleteTableForObject(SharedRealm sharedRealm, String class | |||
return nativeDeleteTableForObject(sharedRealm.getNativePtr(), className); | |||
} | |||
|
|||
/** | |||
* Try to grab a exclusive lock on the given Realm file. If the lock can be acquired, the {@code runnable} will |
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.
willl be
@@ -71,6 +73,19 @@ public static boolean deleteTableForObject(SharedRealm sharedRealm, String class | |||
return nativeDeleteTableForObject(sharedRealm.getNativePtr(), className); | |||
} | |||
|
|||
/** | |||
* Try to grab a exclusive lock on the given Realm file. If the lock can be acquired, the {@code runnable} will | |||
* executed while the lock is hold. The lock will ensure no one else can read or write the Realm file at the same |
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.
to the Realm file
@@ -196,6 +197,7 @@ private SharedRealm(OsRealmConfig osRealmConfig) { | |||
this.schemaInfo = new OsSchemaInfo(nativeGetSchemaInfo(nativePtr), this); | |||
this.context = osRealmConfig.getContext(); | |||
this.context.addReference(this); | |||
this.isPhantomRef = true; |
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.
Would isManagedByPhantomRef
be more clear? Right now it feels a bit unclear to me
@@ -482,8 +499,13 @@ private void executePendingRowQueries() { | |||
@SuppressWarnings("unused") | |||
private static void runMigrationCallback(long nativeSharedRealmPtr, OsRealmConfig osRealmConfig, MigrationCallback callback, | |||
long oldVersion) { | |||
callback.onMigrationNeeded(new SharedRealm(nativeSharedRealmPtr, osRealmConfig), oldVersion, | |||
osRealmConfig.getRealmConfiguration().getSchemaVersion()); | |||
SharedRealm sharedRealm = new SharedRealm(nativeSharedRealmPtr, osRealmConfig); |
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.
Not sure I understand the reason for making this change?
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.
complex story.
take the migration for example, ObjectStore will pass a SharedRealm
instance to the callback
to make the life control easier, i created a new pointer from that instance and pass it to java, so it can be managed like a normal SharedRealm
, user will never meet a invalid pointer of SharedRealm
even when he passed that SharedRealm
instance out.
But then it is a problem when exception happens in migration block. after exception thrown, getInstance()
hasn't return anything yet, so user doesn't have to call close()
. But there is a SharedRealm
pointer created in the migration callback which is waiting for the GC to be deleted. That means, from OS's RealmCoordinator pointer of view, there is still one SharedRealm
is not dealloced. -- call deleteRealm()
at this time will throw.
But i think i still miss one thing in this modification, SharedRealm.isClosed()
should return true after delete()
called to avoid use pass the Realm
/DyanmicRealm
instance out of the callback and see a deleted pointer.
* To delete Realm and related temporary files. This must be called in | ||
* {@link OsObjectStore#callWithLock(RealmConfiguration, Runnable)}'s callback. | ||
* | ||
* @return {@code true} if the realm file is deleted. Temporary file deletion failure will not impact on return |
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.
on return -> the return
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.
Also, it will impact the return value right? false
is returned if they cannot be deleted?
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.
no, it will always return true if the realm file has been deleted.
I think it is just good enough to ensure the realm file is deleted since they take most of disk spaces.
Also, for the Realm on external storage, the temporary files are not deleted since we don't know where are they. (the logic of this part is decided by core at runtiem :( where all the fifo permission nightmare come from)
Since it is not necessary for doing that. The newly created SharedRealm should be immediately deleted before the callback returns. Set the BaseRealm.sharedRealm to null before callback returns.
So the Java OsSharedRealm instance will still never hold a invalid SharedRealm pointer.
@Override | ||
public void create() throws IOException { | ||
super.create(); | ||
tempFolder = new File(super.getRoot(), testName); |
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.
nice move 👍
// Close the SharedRealm. Otherwise it will only be closed when the Java OsSharedRealm gets GCed. And | ||
// that will be too late. | ||
TERMINATE_JNI_IF_JAVA_EXCEPTION_OCCURRED( | ||
env, [&new_shared_realm_ptr]() { (*new_shared_realm_ptr)->close(); }); |
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.
do we need to delete new_shared_realm_ptr
as well?
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.
nope~ it will be still handled by the GC and phantom refs.
// Close the SharedRealm. Otherwise it will only be closed when the Java OsSharedRealm gets GCed. And | ||
// that will be too late. | ||
TERMINATE_JNI_IF_JAVA_EXCEPTION_OCCURRED( | ||
env, [&new_shared_realm_ptr]() { (*new_shared_realm_ptr)->close(); }); |
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.
ditto
assetFile calls are process safe.
thread is still opened.
callbacks, eg.: initialization_data_callback and migration_callback.