Skip to content
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

Made Realm.compactRealmFile() more failure resilient. #955

Merged
merged 13 commits into from
Apr 16, 2015
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
0.81
* Realm.compactRealmFile() now uses Realm Core's compact() method which is more failure resilient.
* Realm.copyToRealm() now correctly handles referenced child objects that are already in the Realm.
* The ARM64 binary is now properly a part of the Eclipse distribution package.
* A RealmMigrationExceptionNeeded is now properly thrown if @Index and @PrimaryKey are not set correctly during a migration.
Expand Down
12 changes: 12 additions & 0 deletions realm-jni/src/io_realm_internal_SharedGroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,15 @@ JNIEXPORT jstring JNICALL Java_io_realm_internal_SharedGroup_nativeGetDefaultRep
return 0;
#endif
}

JNIEXPORT jboolean JNICALL Java_io_realm_internal_SharedGroup_nativeCompact(
JNIEnv* env, jobject, jlong native_ptr)
{
TR_ENTER_PTR(native_ptr)
try {
return SG(native_ptr)->compact(); // throws
}
CATCH_FILE()
CATCH_STD()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add CATCH_FILE() too.

return false;
}
8 changes: 8 additions & 0 deletions realm-jni/src/io_realm_internal_SharedGroup.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

103 changes: 71 additions & 32 deletions realm/src/androidTest/java/io/realm/RealmTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@
import android.test.AndroidTestCase;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
Expand All @@ -37,9 +34,9 @@

import io.realm.entities.AllTypes;
import io.realm.entities.AllTypesPrimaryKey;
import io.realm.entities.Cat;
import io.realm.entities.CyclicType;
import io.realm.entities.CyclicTypePrimaryKey;
import io.realm.entities.Cat;
import io.realm.entities.Dog;
import io.realm.entities.DogPrimaryKey;
import io.realm.entities.NonLatinFieldNames;
Expand Down Expand Up @@ -92,30 +89,30 @@ protected void tearDown() throws Exception {
}
}

private void populateTestRealm(int objects) {
testRealm.beginTransaction();
testRealm.allObjects(AllTypes.class).clear();
testRealm.allObjects(NonLatinFieldNames.class).clear();
private void populateTestRealm(Realm realm, int objects) {
realm.beginTransaction();
realm.allObjects(AllTypes.class).clear();
realm.allObjects(NonLatinFieldNames.class).clear();
for (int i = 0; i < objects; ++i) {
AllTypes allTypes = testRealm.createObject(AllTypes.class);
AllTypes allTypes = realm.createObject(AllTypes.class);
allTypes.setColumnBoolean((i % 3) == 0);
allTypes.setColumnBinary(new byte[]{1, 2, 3});
allTypes.setColumnDate(new Date());
allTypes.setColumnDouble(3.1415);
allTypes.setColumnFloat(1.234567f + i);
allTypes.setColumnString("test data " + i);
allTypes.setColumnLong(i);
NonLatinFieldNames nonLatinFieldNames = testRealm.createObject(NonLatinFieldNames.class);
NonLatinFieldNames nonLatinFieldNames = realm.createObject(NonLatinFieldNames.class);
nonLatinFieldNames.set델타(i);
nonLatinFieldNames.setΔέλτα(i);
nonLatinFieldNames.set베타(1.234567f + i);
nonLatinFieldNames.setΒήτα(1.234567f + i);
}
testRealm.commitTransaction();
realm.commitTransaction();
}

private void populateTestRealm() {
populateTestRealm(TEST_DATA_SIZE);
populateTestRealm(testRealm, TEST_DATA_SIZE);
}


Expand Down Expand Up @@ -287,7 +284,7 @@ public void testShouldReturnResultSet() {

// Note that this test is relying on the values set while initializing the test dataset
public void testQueriesResults() throws IOException {
populateTestRealm(159);
populateTestRealm(testRealm, 159);
RealmResults<AllTypes> resultList = testRealm.where(AllTypes.class).equalTo(FIELD_LONG, 33).findAll();
assertEquals(1, resultList.size());

Expand Down Expand Up @@ -937,14 +934,66 @@ public void testWriteCopyTo() throws IOException {
}
}

public void testCompactRealmFile() throws IOException {
final String copyRealm = "copy.realm";
fileCopy(
new File(getContext().getFilesDir(), Realm.DEFAULT_REALM_NAME),
new File(getContext().getFilesDir(), copyRealm));
long before = new File(getContext().getFilesDir(), copyRealm).length();
assertTrue(Realm.compactRealmFile(getContext()));
long after = new File(getContext().getFilesDir(), copyRealm).length();

public void testCompactRealmFileThrowsIfOpen() throws IOException {
try {
Realm.compactRealmFile(getContext());
fail();
} catch (IllegalStateException expected) {
}
}

public void testCompactEncryptedEmptyRealmFile() {
String REALM_NAME = "enc.realm";
Realm.deleteRealmFile(getContext(), REALM_NAME);
byte[] key = new byte[64];
new Random(42).nextBytes(key);
Realm realm = Realm.getInstance(getContext(), REALM_NAME, key);
realm.close();
// TODO: remove try/catch block when compacting encrypted Realms is supported
try {
assertTrue(Realm.compactRealmFile(getContext(), REALM_NAME, key));
fail();
} catch (IllegalArgumentException expected) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead it should be

Realm.compactRealmFile(getContext(), REALM_NAME, key);
fail();

}
}

public void testCompactEncryptedPopulatedRealmFile() {
String REALM_NAME = "enc.realm";
Realm.deleteRealmFile(getContext(), REALM_NAME);
byte[] key = new byte[64];
new Random(42).nextBytes(key);
Realm realm = Realm.getInstance(getContext(), REALM_NAME, key);
populateTestRealm(realm, 100);
realm.close();
// TODO: remove try/catch block when compacting encrypted Realms is supported
try {
assertTrue(Realm.compactRealmFile(getContext(), REALM_NAME, key));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

fail();
} catch (IllegalArgumentException expected) {
}
}

public void testCompactEmptyRealmFile() throws IOException {
final String REALM_NAME = "test.realm";
Realm.deleteRealmFile(getContext(), REALM_NAME);
Realm realm = Realm.getInstance(getContext(), REALM_NAME);
realm.close();
long before = new File(getContext().getFilesDir(), REALM_NAME).length();
assertTrue(Realm.compactRealmFile(getContext(), REALM_NAME));
long after = new File(getContext().getFilesDir(), REALM_NAME).length();
assertTrue(before >= after);
}

public void testCompactPopulateRealmFile() throws IOException {
final String REALM_NAME = "test.realm";
Realm.deleteRealmFile(getContext(), REALM_NAME);
Realm realm = Realm.getInstance(getContext(), REALM_NAME);
populateTestRealm(realm, 100);
realm.close();
long before = new File(getContext().getFilesDir(), REALM_NAME).length();
assertTrue(Realm.compactRealmFile(getContext(), REALM_NAME));
long after = new File(getContext().getFilesDir(), REALM_NAME).length();
assertTrue(before >= after);
}

Expand Down Expand Up @@ -991,7 +1040,7 @@ public void testCopManagedObjectToOtherRealm() {

public void testCopyToRealmObject() {
Date date = new Date();
date.setTime(1000); // Remove ms. precission as Realm doesn't support it yet.
date.setTime(1000); // Remove ms. precision as Realm doesn't support it yet.
Dog dog = new Dog();
dog.setName("Fido");
RealmList<Dog> list = new RealmList<Dog>();
Expand Down Expand Up @@ -1360,16 +1409,6 @@ public void testCopyOrUpdateIterableChildObjects() {
assertEquals(1, testRealm.allObjects(DogPrimaryKey.class).size());
}

private void fileCopy(File src, File dst) throws IOException {
FileInputStream inStream = new FileInputStream(src);
FileOutputStream outStream = new FileOutputStream(dst);
FileChannel inChannel = inStream.getChannel();
FileChannel outChannel = outStream.getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
inStream.close();
outStream.close();
}

public void testOpeningOfEncryptedRealmWithDifferentKeyInstances() {
byte[] key1 = new byte[64];
byte[] key2 = new byte[64];
Expand Down
82 changes: 51 additions & 31 deletions realm/src/main/java/io/realm/Realm.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import android.os.Looper;
import android.os.Message;
import android.util.JsonReader;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
Expand All @@ -46,7 +45,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

Expand Down Expand Up @@ -1711,72 +1709,94 @@ public static synchronized boolean deleteRealmFile(File realmFile) {
boolean deleteResult = fileToDelete.delete();
if (!deleteResult) {
result = false;
Log.w(TAG, "Could not delete the file " + fileToDelete);
RealmLog.w("Could not delete the file " + fileToDelete);
}
}
}
return result;
}

/**
* Compact a realm file. A realm file usually contain free/unused space.
* Compact a Realm file. A Realm file usually contain free/unused space.
* This method removes this free space and the file size is thereby reduced.
* Objects within the realm files are untouched.
* Objects within the Realm files are untouched.
* <p>
* The file must be closed before this method is called.<br>
* The file system should have free space for at least a copy of the realm file.<br>
* The file system should have free space for at least a copy of the Realm file.<br>
* The realm file is left untouched if any file operation fails.<br>
* Currently it is not possible to compact an encrypted Realm.<br>
*
* @param context an Android {@link android.content.Context}
* @param fileName the name of the file to compact
* @param key Key for opening an encrypted Realm.
* @return true if successful, false if any file operation failed
*
* @throws IllegalStateException if trying to compact a Realm that is already open.
*/
public static synchronized boolean compactRealmFile(Context context, String fileName) {
File realmFile = new File(context.getFilesDir(), fileName);
File tmpFile = new File(
context.getFilesDir(),
String.valueOf(System.currentTimeMillis()) + UUID.randomUUID() + ".realm");
public static synchronized boolean compactRealmFile(Context context, String fileName, byte[] key) {
if (key != null) {
throw new IllegalArgumentException("Cannot currently compact an encrypted Realm.");
}

Realm realm = null;
File realmFile = new File(context.getFilesDir(), fileName);
String path = realmFile.getAbsolutePath();
if (openRealms.get(path.hashCode()).get() > 0) {
throw new IllegalStateException("Cannot compact an open Realm");
}
SharedGroup sharedGroup = null;
boolean result = false;
try {
realm = Realm.getInstance(context, fileName);
realm.writeCopyTo(tmpFile);
if (!realmFile.delete()) {
return false;
}
if (!tmpFile.renameTo(realmFile)) {
return false;
}
} catch (IOException e) {
return false;
sharedGroup = new SharedGroup(path, false, key);
result = sharedGroup.compact();
} finally {
if (realm != null) {
realm.close();
if (sharedGroup != null) {
sharedGroup.close();
}
}
return true;
return result;
}

/**
* Compact a realm file. A realm file usually contain free/unused space.
* Compact a Realm file. A Realm file usually contain free/unused space.
* This method removes this free space and the file size is thereby reduced.
* Objects within the realm files are untouched.
* Objects within the Realm files are untouched.
* <p>
* The file must be closed before this method is called.<br>
* The file system should have free space for at least a copy of the realm file.<br>
* The realm file is left untouched if any file operation fails.<br>
* The file system should have free space for at least a copy of the Realm file.<br>
* The Realm file is left untouched if any file operation fails.<br>
*
* @param context an Android {@link android.content.Context}
* @return true if successful, false if any file operation failed
*
* @throws IllegalStateException if trying to compact a Realm that is already open.
*/
public static boolean compactRealmFile(Context context) {
return compactRealmFile(context, DEFAULT_REALM_NAME);
return compactRealmFile(context, DEFAULT_REALM_NAME, null);
}

/**
* Compact a Realm file. A Realm file usually contain free/unused space.
* This method removes this free space and the file size is thereby reduced.
* Objects within the Realm files are untouched.
* <p>
* The file must be closed before this method is called.<br>
* The file system should have free space for at least a copy of the Realm file.<br>
* The Realm file is left untouched if any file operation fails.<br>
*
* @param context an Android {@link android.content.Context}
* @param fileName the name of the file to compact
* @return true if successful, false if any file operation failed
*
* @throws IllegalStateException if trying to compact a Realm that is already open.
*/
public static synchronized boolean compactRealmFile(Context context, String fileName) {
return compactRealmFile(context, fileName, null);
}

/**
* Returns the absolute path to where this Realm is persisted on disk.
*
* @return The absolute path to the realm file.
* @return The absolute path to the Realm file.
*/
public String getPath() {
return path;
Expand Down
12 changes: 12 additions & 0 deletions realm/src/main/java/io/realm/internal/SharedGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,16 @@ public void reserve(long bytes) {
nativeReserve(nativePtr, bytes);
}

/**
* Compacts a shared group. This will block access to the shared group until done.
*
* @return True if compaction succeeded, false otherwise.
* @throws RuntimeException if using this within either a read or or write transaction.
*/
public boolean compact() {
return nativeCompact(nativePtr);
}

private native void nativeReserve(long nativePtr, long bytes);

private native boolean nativeHasChanged(long nativePtr);
Expand All @@ -243,6 +253,8 @@ private native long nativeCreate(String databaseFile,
boolean enableReplication,
byte[] key);

private native boolean nativeCompact(long nativePtr);

private void checkNativePtrNotZero() {
if (this.nativePtr == 0) {
throw new IOError(new RealmIOException("Realm could not be opened"));
Expand Down