java/syncbase: Join Or Create Userdata. UUID collection+syncgroup
Userdata will avoid the syncgroup merge issue by using a join or
create pattern. This collection should be used if one wants to
have a user-only collection.
db.collection(name) => db.createCollection()
Every non-userdata collection name will be assigned a UUID.
Syncgroups will use the same name, and thus avoid the merge
problem.
Drawbacks:
- Watch is a little harder since you can't expect any particular name,
only a prefix (if you specify one).
- Collection creation in a batch is also a little harder to use since
we are no longer idempotent when calling db.collection.
Change-Id: I35bc93519667110914ea7c52c95cb6c006ee3bb8
diff --git a/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java b/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
index 81b7f1a..b60b504 100644
--- a/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
+++ b/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
@@ -4,6 +4,8 @@
package io.v.syncbase;
+import java.util.UUID;
+
import io.v.syncbase.core.VError;
import io.v.syncbase.exception.SyncbaseException;
@@ -25,7 +27,8 @@
* @throws IllegalArgumentException if opts.withoutSyncgroup false
*/
@Override
- public Collection collection(String name, CollectionOptions opts) throws SyncbaseException {
+ public Collection createCollection(CollectionOptions opts) throws SyncbaseException {
+ String name = opts.prefix + "_" + UUID.randomUUID().toString().replaceAll("-", "");
if (!opts.withoutSyncgroup) {
throw new IllegalArgumentException("Cannot create syncgroup in a batch");
}
diff --git a/syncbase/src/main/java/io/v/syncbase/Database.java b/syncbase/src/main/java/io/v/syncbase/Database.java
index eda76d9..e86141f 100644
--- a/syncbase/src/main/java/io/v/syncbase/Database.java
+++ b/syncbase/src/main/java/io/v/syncbase/Database.java
@@ -12,6 +12,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import java.util.concurrent.ExecutionException;
import io.v.syncbase.core.CollectionRowPattern;
@@ -50,7 +51,12 @@
}
@Override
- public Collection collection(String name, CollectionOptions opts) throws SyncbaseException {
+ public Collection createCollection(CollectionOptions opts) throws SyncbaseException {
+ String name = opts.prefix + "_" + UUID.randomUUID().toString().replaceAll("-", "");
+ return createNamedCollection(name, opts);
+ }
+
+ Collection createNamedCollection(String name, CollectionOptions opts) throws SyncbaseException {
Collection res = getCollection(new Id(Syncbase.getPersonalBlessingString(), name));
res.createIfMissing();
// TODO(sadovsky): Unwind collection creation on syncgroup creation failure? It would be
@@ -61,6 +67,15 @@
return res;
}
+
+ /**
+ * Returns a reference to the userdata collection. Returns null if the user is not currently
+ * logged in.
+ */
+ public Collection getUserdataCollection() throws SyncbaseException {
+ return getCollection(new Id(Syncbase.getPersonalBlessingString(), Syncbase.USERDATA_NAME));
+ }
+
/**
* FOR ADVANCED USERS. Options for syncgroup creation.
*/
@@ -95,7 +110,7 @@
// Remember this syncgroup in the userdata collection. The value doesn't matter, but since
// VOM won't accept null, use a boolean.
// Note: We may eventually want to use the value to deal with rejected invitations.
- Syncbase.sUserdataCollection.put(id.encode(), true);
+ Syncbase.addToUserdata(id);
return syncgroup;
}
@@ -258,8 +273,9 @@
expectedBlessings.add(Syncbase.sOpts.getCloudBlessingString());
}
coreSyncgroup.join(publishName, expectedBlessings, new SyncgroupMemberInfo());
- } catch (VError vError) {
- cb.onFailure(vError);
+ Syncbase.addToUserdata(invite.getId());
+ } catch (VError | SyncbaseException e) {
+ cb.onFailure(e);
return;
}
cb.onSuccess(new Syncgroup(coreSyncgroup, database));
@@ -428,9 +444,14 @@
@Override
public void onChange(io.v.syncbase.core.WatchChange coreWatchChange) {
- // TODO(razvanm): Ignore changes to userdata collection.
- if (coreWatchChange.entityType !=
- io.v.syncbase.core.WatchChange.EntityType.ROOT) {
+ boolean isRoot = coreWatchChange.entityType ==
+ io.v.syncbase.core.WatchChange.EntityType.ROOT;
+ boolean isUserdataCollectionRow =
+ coreWatchChange.entityType ==
+ io.v.syncbase.core.WatchChange.EntityType.ROW &&
+ coreWatchChange.collection.name.equals(Syncbase.USERDATA_NAME) &&
+ coreWatchChange.row.startsWith(Syncbase.USERDATA_COLLECTION_PREFIX);
+ if (!isRoot && !isUserdataCollectionRow) {
mBatch.add(new WatchChange(coreWatchChange));
}
if (!coreWatchChange.continued) {
diff --git a/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java b/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
index b31f96b..90a8761 100644
--- a/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
+++ b/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
@@ -19,6 +19,8 @@
public abstract class DatabaseHandle {
private final io.v.syncbase.core.DatabaseHandle mCoreDatabaseHandle;
+ protected static final String DEFAULT_COLLECTION_PREFIX = "cx";
+
DatabaseHandle(io.v.syncbase.core.DatabaseHandle coreDatabaseHandle) {
mCoreDatabaseHandle = coreDatabaseHandle;
}
@@ -35,32 +37,38 @@
*/
public static class CollectionOptions {
public boolean withoutSyncgroup;
+ public String prefix = DEFAULT_COLLECTION_PREFIX;
public CollectionOptions setWithoutSyncgroup(boolean value) {
withoutSyncgroup = value;
return this;
}
+
+ public CollectionOptions setPrefix(String value) {
+ prefix = value;
+ return this;
+ }
}
/**
- * Creates a collection and an associated syncgroup, as needed. Idempotent. The id of the new
- * collection will include the creator's user id and the given collection name. Upon creation,
- * both the collection and syncgroup are {@code READ_WRITE} for the creator. Setting
- * {@code opts.withoutSyncgroup} prevents syncgroup creation. May only be called within a batch
- * if {@code opts.withoutSyncgroup} is set.
+ * Creates a new collection and an associated syncgroup.
+ * The id of the collection will include the creator's user id and its name will be a UUID.
+ * Upon creation, both the collection and syncgroup are {@code READ_WRITE} for the creator.
+ * Setting {@code opts.withoutSyncgroup} prevents syncgroup creation.
+ * Setting {@code opts.prefix} will assign a UUID name starting with that prefix.
+ * May only be called within a batch if {@code opts.withoutSyncgroup} is set.
*
- * @param name name of the collection
* @param opts options for collection creation
* @return the collection handle
*/
- public abstract Collection collection(String name, CollectionOptions opts)
+ public abstract Collection createCollection(CollectionOptions opts)
throws SyncbaseException;
/**
* Calls {@code collection(name, opts)} with default {@code CollectionOptions}.
*/
- public Collection collection(String name) throws SyncbaseException {
- return collection(name, new CollectionOptions());
+ public Collection createCollection() throws SyncbaseException {
+ return createCollection(new CollectionOptions());
}
/**
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncbase.java b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
index be4981f..0f7caab 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncbase.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
@@ -117,8 +117,11 @@
static final String
TAG = "syncbase",
DIR_NAME = "syncbase",
- DB_NAME = "db",
- USERDATA_SYNCGROUP_NAME = "userdata__";
+ DB_NAME = "db";
+
+ public static final String
+ USERDATA_NAME = "userdata__",
+ USERDATA_COLLECTION_PREFIX = "__collections/";
private static Map selfAndCloud() throws SyncbaseException {
return ImmutableMap.of(Permissions.IN,
@@ -150,7 +153,6 @@
*/
public static Database database() throws SyncbaseException {
try {
-
if (!isLoggedIn()) {
return null;
}
@@ -186,7 +188,11 @@
* Returns the currently logged in user.
*/
public static User getLoggedInUser() {
- throw new UnsupportedOperationException("Not implemented");
+ try {
+ return new User(getAliasFromBlessingPattern(getPersonalBlessingString()));
+ } catch (SyncbaseException e) {
+ return null;
+ }
}
public interface LoginCallback {
@@ -247,14 +253,19 @@
return;
}
sDatabase.createIfMissing();
- sUserdataCollection = sDatabase.collection(
- USERDATA_SYNCGROUP_NAME,
+ sUserdataCollection = sDatabase.createNamedCollection(
+ USERDATA_NAME,
new DatabaseHandle.CollectionOptions().setWithoutSyncgroup(true));
if (!sOpts.disableUserdataSyncgroup) {
Syncgroup syncgroup = sUserdataCollection.getSyncgroup();
- // TODO(razvanm): First we need to try to join, then we need to try to
- // create the syncgroup.
- syncgroup.createIfMissing(ImmutableList.of(sUserdataCollection));
+ // Join-Or-Create pattern. If join fails, create the syncgroup instead.
+ // Note: Syncgroup merge does not exist yet, so this may potentially lead
+ // to split-brain syncgroups. This is exacerbated by lack of cloud instance.
+ try {
+ syncgroup.join();
+ } catch(VError e) {
+ syncgroup.createIfMissing(ImmutableList.of(sUserdataCollection));
+ }
sDatabase.addWatchChangeHandler(new UserdataWatchHandler());
}
sOpts.callbackExecutor.execute(new Runnable() {
@@ -288,11 +299,14 @@
private void onWatchChange(Iterator<WatchChange> changes) {
WatchChange watchChange = changes.next();
- if (watchChange.getCollectionId().getName().equals(USERDATA_SYNCGROUP_NAME) &&
+ if (watchChange.getCollectionId().getName().equals(USERDATA_NAME) &&
watchChange.getEntityType() == WatchChange.EntityType.ROW &&
- watchChange.getChangeType() == WatchChange.ChangeType.PUT) {
+ watchChange.getChangeType() == WatchChange.ChangeType.PUT &&
+ watchChange.getRowKey().startsWith(USERDATA_COLLECTION_PREFIX)) {
try {
- sDatabase.getSyncgroup(Id.decode(watchChange.getRowKey())).join();
+ String encodedId = watchChange.getRowKey().
+ substring(Syncbase.USERDATA_COLLECTION_PREFIX.length());
+ sDatabase.getSyncgroup(Id.decode(encodedId)).join();
} catch (VError vError) {
vError.printStackTrace();
System.err.println(vError.toString());
@@ -301,6 +315,10 @@
}
}
+ static void addToUserdata(Id id) throws SyncbaseException {
+ sUserdataCollection.put(Syncbase.USERDATA_COLLECTION_PREFIX + id.encode(), true);
+ }
+
/**
* Scans the neighborhood for nearby users.
*
diff --git a/syncbase/src/test/java/io/v/syncbase/BatchDatabaseTest.java b/syncbase/src/test/java/io/v/syncbase/BatchDatabaseTest.java
index c598c1a..1569d10 100644
--- a/syncbase/src/test/java/io/v/syncbase/BatchDatabaseTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/BatchDatabaseTest.java
@@ -44,7 +44,7 @@
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Cannot create syncgroup in a batch");
- batch.collection("aCollection", new CollectionOptions());
+ batch.createCollection(new CollectionOptions());
}
}
diff --git a/syncbase/src/test/java/io/v/syncbase/CollectionTest.java b/syncbase/src/test/java/io/v/syncbase/CollectionTest.java
index 76312e7..6be3f76 100644
--- a/syncbase/src/test/java/io/v/syncbase/CollectionTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/CollectionTest.java
@@ -50,7 +50,7 @@
thrown.expectMessage("invalid name");
// Create with invalid name
- Syncbase.database().collection("name with spaces", new CollectionOptions());
+ Syncbase.database().createNamedCollection("name with spaces", new CollectionOptions());
}
@Test
@@ -59,7 +59,7 @@
BatchDatabase batch = Syncbase.database().beginBatch(new Database.BatchOptions());
// Create collection without a syncgroup
- Collection collection = batch.collection("collectionName",
+ Collection collection = batch.createCollection(
new CollectionOptions().setWithoutSyncgroup(true));
thrown.expect(IllegalArgumentException.class);
diff --git a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
index 82f410e..f8fd471 100644
--- a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
@@ -60,6 +60,21 @@
return res;
}
+ private static boolean idsMatch(Iterable<Id> ids, String blessing, List<String> prefixes) {
+ int i = 0;
+ for (Id id : ids) {
+ if (!idMatch(id, blessing, prefixes.get(i))) {
+ return false;
+ }
+ i++;
+ }
+ return prefixes.size() == i; // Every id matches, and all prefixes were used.
+ }
+
+ private static boolean idMatch(Id id, String blessing, String prefix) {
+ return id.getName().startsWith(prefix) && id.getBlessing().equals(blessing);
+ }
+
private static Iterable<Id> getSyncgroupIds(Database db) throws SyncbaseException {
List<Id> res = new ArrayList<>();
for (Iterator<Syncgroup> it = db.getSyncgroups(); it.hasNext(); ) {
@@ -77,35 +92,35 @@
public void testCreateAndGetCollections() throws Exception {
Database db = createDatabase();
assertNotNull(db);
- DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions();
- opts.withoutSyncgroup = true;
- Collection cxA = db.collection("a", opts);
+ DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions().
+ setWithoutSyncgroup(true).setPrefix("a");
+
+ Collection cxA = db.createCollection(opts);
assertNotNull(cxA);
- // TODO(sadovsky): Should we omit the userdata collection?
- assertThat(getCollectionIds(db)).containsExactly(
- new Id(Syncbase.getPersonalBlessingString(), "a"),
- new Id(Syncbase.getPersonalBlessingString(), "userdata__"));
- db.collection("b", opts);
- assertThat(getCollectionIds(db)).containsExactly(
- new Id(Syncbase.getPersonalBlessingString(), "a"),
- new Id(Syncbase.getPersonalBlessingString(), "b"),
- new Id(Syncbase.getPersonalBlessingString(), "userdata__"));
- // Note, createDatabase() sets disableSyncgroupPublishing to true, so db.collection(name) is
- // a purely local operation.
- db.collection("c");
- assertThat(getCollectionIds(db)).containsExactly(
- new Id(Syncbase.getPersonalBlessingString(), "a"),
- new Id(Syncbase.getPersonalBlessingString(), "b"),
- new Id(Syncbase.getPersonalBlessingString(), "c"),
- new Id(Syncbase.getPersonalBlessingString(), "userdata__"));
- Collection secondCxA = db.collection("a", opts);
- assertEquals(cxA.getId(), secondCxA.getId());
+ assertTrue(idsMatch(getCollectionIds(db), Syncbase.getPersonalBlessingString(),
+ ImmutableList.of("a", Syncbase.USERDATA_NAME)));
+
+ db.createCollection(opts.setPrefix("b"));
+ assertTrue(idsMatch(getCollectionIds(db), Syncbase.getPersonalBlessingString(),
+ ImmutableList.of("a", "b", Syncbase.USERDATA_NAME)));
+
+ // Note, createDatabase() sets disableSyncgroupPublishing to true, so
+ // db.createCollection(opts) is still a purely local operation.
+ opts = new DatabaseHandle.CollectionOptions();
+ db.createCollection(opts.setPrefix("c"));
+ assertTrue(idsMatch(getCollectionIds(db), Syncbase.getPersonalBlessingString(),
+ ImmutableList.of("a", "b", "c", Syncbase.USERDATA_NAME)));
+
+ Collection secondCxA = db.createCollection(opts.setPrefix("a"));
+ assertFalse(cxA.getId().equals(secondCxA.getId()));
+ assertTrue(idsMatch(getCollectionIds(db), Syncbase.getPersonalBlessingString(),
+ ImmutableList.of("a", "a", "b", "c", Syncbase.USERDATA_NAME)));
}
@Test
public void testRowCrudMethods() throws Exception {
Database db = createDatabase();
- Collection cx = db.collection("cx");
+ Collection cx = db.createCollection();
assertNotNull(cx);
assertFalse(cx.exists("foo"));
assertEquals(cx.get("foo", String.class), null);
@@ -141,17 +156,17 @@
Database db = createDatabase();
DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions();
opts.withoutSyncgroup = true;
- Collection cxA = db.collection("a", opts);
- Collection cxB = db.collection("b", opts);
- Collection cxC = db.collection("c");
+ Collection cxA = db.createCollection(opts.setPrefix("a"));
+ Collection cxB = db.createCollection(opts.setPrefix("b"));
+ Collection cxC = db.createCollection(opts.setWithoutSyncgroup(false).setPrefix("c"));
assertNotNull(cxA);
// Note, there's no userdata syncgroup since we set disableUserdataSyncgroup to true.
- assertThat(getSyncgroupIds(db)).containsExactly(
- new Id(Syncbase.getPersonalBlessingString(), "c"));
+ assertTrue(idsMatch(getSyncgroupIds(db), Syncbase.getPersonalBlessingString(),
+ ImmutableList.of("c")));
db.syncgroup("sg1", ImmutableList.of(cxA));
db.syncgroup("sg2", ImmutableList.of(cxA, cxB, cxC));
assertThat(getSyncgroupIds(db)).containsExactly(
- new Id(Syncbase.getPersonalBlessingString(), "c"),
+ new Id(Syncbase.getPersonalBlessingString(), cxC.getSyncgroup().getId().getName()),
new Id(Syncbase.getPersonalBlessingString(), "sg1"),
new Id(Syncbase.getPersonalBlessingString(), "sg2"));
}
@@ -161,8 +176,9 @@
Database db = createDatabase();
final SettableFuture<Void> waitOnInitialState = SettableFuture.create();
final SettableFuture<Void> waitOnChangeBatch = SettableFuture.create();
- Collection collection = db.collection("c");
+ Collection collection = db.createCollection();
collection.put("foo", 1);
+ final String collectionName = collection.getId().getName();
db.addWatchChangeHandler(new Database.WatchChangeHandler() {
@Override
public void onInitialState(Iterator<WatchChange> values) {
@@ -172,13 +188,13 @@
WatchChange watchChange = (WatchChange) values.next();
assertEquals(WatchChange.EntityType.COLLECTION, watchChange.getEntityType());
assertEquals(WatchChange.ChangeType.PUT, watchChange.getChangeType());
- assertEquals("c", watchChange.getCollectionId().getName());
+ assertTrue(watchChange.getCollectionId().getName().equals(collectionName));
// 2nd change: the row for the "foo" key.
assertTrue(values.hasNext());
watchChange = (WatchChange) values.next();
assertEquals(WatchChange.EntityType.ROW, watchChange.getEntityType());
assertEquals(WatchChange.ChangeType.PUT, watchChange.getChangeType());
- assertEquals("c", watchChange.getCollectionId().getName());
+ assertTrue(watchChange.getCollectionId().getName().equals(collectionName));
assertEquals("foo", watchChange.getRowKey());
// TODO(razvanm): Uncomment after the POJO start working.
//assertEquals(1, watchChange.getValue());
@@ -187,13 +203,9 @@
watchChange = (WatchChange) values.next();
assertEquals(WatchChange.EntityType.COLLECTION, watchChange.getEntityType());
assertEquals(WatchChange.ChangeType.PUT, watchChange.getChangeType());
- // 4th change: the userdata collection has a row for "c"'s syncgroup.
- assertTrue(values.hasNext());
- watchChange = (WatchChange) values.next();
- assertEquals(WatchChange.EntityType.ROW, watchChange.getEntityType());
- assertEquals(WatchChange.ChangeType.PUT, watchChange.getChangeType());
- assertEquals("userdata__", watchChange.getCollectionId().getName());
- assertTrue(watchChange.getRowKey().endsWith("c"));
+ assertTrue(watchChange.getCollectionId().getName().equals(Syncbase.USERDATA_NAME));
+ // No more changes.
+ assertFalse(values.hasNext());
waitOnInitialState.set(null);
}
@@ -202,6 +214,7 @@
assertTrue(changes.hasNext());
WatchChange watchChange = changes.next();
assertEquals(WatchChange.ChangeType.DELETE, watchChange.getChangeType());
+ assertTrue(watchChange.getCollectionId().getName().startsWith("c"));
// TODO(razvanm): Uncomment after the POJO start working.
//assertEquals(1, watchChange.getValue());
assertFalse(changes.hasNext());
@@ -224,26 +237,36 @@
@Test
public void testRunInBatch() throws Exception {
Database db = createDatabase();
- db.runInBatch(new Database.BatchOperation() {
+
+ // We need a box class to store the id of the created collection since we want to refer to
+ // it after the batch is over.
+ class TestOperation implements Database.BatchOperation {
+ private Id id;
+
@Override
public void run(BatchDatabase db) {
try {
DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions()
.setWithoutSyncgroup(true);
- db.collection("c", opts).put("foo", 10);
+ Collection c = db.createCollection(opts);
+ c.put("foo", 10);
+ id = c.getId();
} catch (SyncbaseException e) {
e.printStackTrace();
fail(e.toString());
}
}
- });
- assertEquals(db.collection("c").get("foo", Integer.class), Integer.valueOf(10));
+ }
+
+ TestOperation op = new TestOperation();
+ db.runInBatch(op);
+ assertEquals(db.getCollection(op.id).get("foo", Integer.class), Integer.valueOf(10));
}
@Test
public void testSyncgroupInviteUsers() throws Exception {
Database db = createDatabase();
- Collection collection = db.collection("c");
+ Collection collection = db.createCollection();
Syncgroup sg = collection.getSyncgroup();
User alice = new User("alice");