fix(store): allocate graph id before batch writes (#3153)
HStore uses a 2-byte GraphId as the graph-isolation boundary. Before this fix, a new graph whose first write used batch PUT or MERGE could encode data with the reserved missing GraphId 0xFFFE. Multiple graphs could then share the same physical RocksDB key range.
---------
Co-authored-by: imbajin <jin@apache.org>
diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandler.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandler.java
index 8133654..58e0056 100644
--- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandler.java
+++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandler.java
@@ -152,6 +152,7 @@
default void doBatch(String graph, int partId, List<BatchEntry> entryList) {
BusinessHandler.TxBuilder builder = txBuilder(graph, partId);
+ BusinessHandler.Tx transaction = builder.build();
try {
for (BatchEntry b : entryList) {
Key start = b.getStartKey();
@@ -185,12 +186,16 @@
}
}
}
- builder.build().commit();
+ transaction.commit();
} catch (Throwable e) {
String msg =
String.format("graph data %s-%s do batch insert with error:", graph, partId);
log.error(msg, e);
- builder.build().rollback();
+ try {
+ transaction.rollback();
+ } catch (Throwable rollbackError) {
+ e.addSuppressed(rollbackError);
+ }
throw e;
}
}
diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java
index 9287bfe..d66093e 100644
--- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java
+++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java
@@ -38,6 +38,9 @@
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -121,6 +124,8 @@
private static final Map<String, HugeGraphSupplier> GRAPH_SUPPLIER_CACHE =
new ConcurrentHashMap<>();
+ private static final int GRAPH_LOCK_STRIPES = 1024;
+ private static final ReadWriteLock[] GRAPH_LOCKS = createGraphLocks();
private static final int batchSize = 10000;
private static Long indexDataSize = 50 * 1024L;
private static final RocksDBFactory factory = RocksDBFactory.getInstance();
@@ -172,6 +177,20 @@
});
}
+ private static ReadWriteLock[] createGraphLocks() {
+ ReadWriteLock[] locks = new ReadWriteLock[GRAPH_LOCK_STRIPES];
+ for (int i = 0; i < locks.length; i++) {
+ locks[i] = new ReentrantReadWriteLock(true);
+ }
+ return locks;
+ }
+
+ private static ReadWriteLock graphLock(String graph, int partId) {
+ int hash = 31 * partId + graph.hashCode();
+ hash ^= hash >>> 16;
+ return GRAPH_LOCKS[hash & (GRAPH_LOCK_STRIPES - 1)];
+ }
+
public static HugeConfig initRocksdb(Map<String, Object> rocksdbConfig,
RocksdbChangedListener listener) {
// Register rocksdb configuration
@@ -1014,11 +1033,17 @@
public void truncate(String graphName, int partId) throws HgStoreException {
// Each partition corresponds to a rocksdb instance, so the rocksdb instance name is
// rocksdb + partId
- try (RocksDBSession dbSession = getSession(graphName, partId)) {
- dbSession.sessionOp().deleteRange(keyCreator.getStartKey(partId, graphName),
- keyCreator.getEndKey(partId, graphName));
- // Release map ID
- keyCreator.delGraphId(partId, graphName);
+ Lock lifecycleLock = graphLock(graphName, partId).writeLock();
+ lifecycleLock.lock();
+ try {
+ try (RocksDBSession dbSession = getSession(graphName, partId)) {
+ dbSession.sessionOp().deleteRange(keyCreator.getStartKey(partId, graphName),
+ keyCreator.getEndKey(partId, graphName));
+ // Release map ID
+ keyCreator.delGraphId(partId, graphName);
+ }
+ } finally {
+ lifecycleLock.unlock();
}
}
@@ -1287,7 +1312,7 @@
@Override
public TxBuilder txBuilder(String graph, int partId) throws HgStoreException {
- return new TxBuilderImpl(graph, partId, getSession(graph, partId));
+ return new TxBuilderImpl(graph, partId);
}
@Override
@@ -1564,20 +1589,42 @@
private final int partId;
private final RocksDBSession dbSession;
private final SessionOperator op;
+ private final Lock lifecycleLock;
+ private boolean completed;
- private TxBuilderImpl(String graph, int partId, RocksDBSession dbSession) {
+ private TxBuilderImpl(String graph, int partId) {
this.graph = graph;
this.partId = partId;
- this.dbSession = dbSession;
- this.op = this.dbSession.sessionOp();
- this.op.prepare();
+ this.lifecycleLock = graphLock(graph, partId).readLock();
+ this.lifecycleLock.lock();
+
+ RocksDBSession session = null;
+ SessionOperator operator = null;
+ try {
+ session = getSession(graph, partId);
+ operator = session.sessionOp();
+ operator.prepare();
+ } catch (RuntimeException | Error e) {
+ try {
+ if (session != null) {
+ session.close();
+ }
+ } catch (Throwable closeError) {
+ e.addSuppressed(closeError);
+ } finally {
+ this.lifecycleLock.unlock();
+ }
+ throw e;
+ }
+ this.dbSession = session;
+ this.op = operator;
}
@Override
public TxBuilder put(int code, String table, byte[] key, byte[] value) throws
HgStoreException {
try {
- byte[] targetKey = keyCreator.getKey(this.partId, graph, code, key);
+ byte[] targetKey = keyCreator.getKeyOrCreate(this.partId, graph, code, key);
this.op.put(table, targetKey, value);
} catch (DBStoreException e) {
throw new HgStoreException(HgStoreException.EC_RKDB_DOPUT_FAIL, e.toString());
@@ -1642,7 +1689,7 @@
HgStoreException {
try {
- byte[] targetKey = keyCreator.getKey(this.partId, graph, code, key);
+ byte[] targetKey = keyCreator.getKeyOrCreate(this.partId, graph, code, key);
op.merge(table, targetKey, value);
} catch (DBStoreException e) {
throw new HgStoreException(HgStoreException.EC_RKDB_DOMERGE_FAIL, e.toString());
@@ -1655,21 +1702,37 @@
return new Tx() {
@Override
public void commit() throws HgStoreException {
+ if (completed) {
+ return;
+ }
op.commit(); // After an exception occurs in commit, rollback must be
// called, otherwise it will cause the lock not to be released.
- dbSession.close();
+ completed = true;
+ release();
}
@Override
public void rollback() throws HgStoreException {
+ if (completed) {
+ return;
+ }
try {
op.rollback();
} finally {
- dbSession.close();
+ completed = true;
+ release();
}
}
};
}
+
+ private void release() {
+ try {
+ this.dbSession.close();
+ } finally {
+ this.lifecycleLock.unlock();
+ }
+ }
}
public static void clearCache() {
diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DataManagerImpl.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DataManagerImpl.java
index 31f111d..3a4b5ac 100644
--- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DataManagerImpl.java
+++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DataManagerImpl.java
@@ -222,12 +222,22 @@
@Override
public void write(BatchPutRequest request) {
- BusinessHandler.TxBuilder tx =
+ BusinessHandler.TxBuilder builder =
businessHandler.txBuilder(request.getGraphName(), request.getPartitionId());
- for (BatchPutRequest.KV kv : request.getEntries()) {
- tx.put(kv.getCode(), kv.getTable(), kv.getKey(), kv.getValue());
+ BusinessHandler.Tx transaction = builder.build();
+ try {
+ for (BatchPutRequest.KV kv : request.getEntries()) {
+ builder.put(kv.getCode(), kv.getTable(), kv.getKey(), kv.getValue());
+ }
+ transaction.commit();
+ } catch (Throwable e) {
+ try {
+ transaction.rollback();
+ } catch (Throwable rollbackError) {
+ e.addSuppressed(rollbackError);
+ }
+ throw e;
}
- tx.build().commit();
}
@Override
diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DefaultDataMover.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DefaultDataMover.java
index 11f0669..8ba7487 100644
--- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DefaultDataMover.java
+++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DefaultDataMover.java
@@ -221,12 +221,22 @@
@Override
public void doWriteData(BatchPutRequest request) {
- BusinessHandler.TxBuilder tx =
+ BusinessHandler.TxBuilder builder =
businessHandler.txBuilder(request.getGraphName(), request.getPartitionId());
- for (BatchPutRequest.KV kv : request.getEntries()) {
- tx.put(kv.getCode(), kv.getTable(), kv.getKey(), kv.getValue());
+ BusinessHandler.Tx transaction = builder.build();
+ try {
+ for (BatchPutRequest.KV kv : request.getEntries()) {
+ builder.put(kv.getCode(), kv.getTable(), kv.getKey(), kv.getValue());
+ }
+ transaction.commit();
+ } catch (Throwable e) {
+ try {
+ transaction.rollback();
+ } catch (Throwable rollbackError) {
+ e.addSuppressed(rollbackError);
+ }
+ throw e;
}
- tx.build().commit();
}
@Override
diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java
index 3a9e2c1..3c95919 100644
--- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java
+++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java
@@ -17,6 +17,8 @@
package org.apache.hugegraph.store.meta;
+import static org.apache.hugegraph.store.constant.HugeServerTables.VERTEX_TABLE;
+
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
@@ -128,8 +130,13 @@
private boolean checkCount(long l) {
var start = new byte[2];
Bits.putShort(start, 0, (short) l);
- try (var itr = sessionBuilder.getSession(partitionId).sessionOp().scan("g+v", start)) {
- return itr == null || !itr.hasNext();
+ try (var session = sessionBuilder.getSession(partitionId)) {
+ if (!session.tableIsExist(VERTEX_TABLE)) {
+ return true;
+ }
+ try (var itr = session.sessionOp().scan(VERTEX_TABLE, start)) {
+ return itr == null || !itr.hasNext();
+ }
}
}
diff --git a/hugegraph-store/hg-store-test/pom.xml b/hugegraph-store/hg-store-test/pom.xml
index 8b7f10d..ed91e01 100644
--- a/hugegraph-store/hg-store-test/pom.xml
+++ b/hugegraph-store/hg-store-test/pom.xml
@@ -244,6 +244,7 @@
</testClassesDirectory>
<includes>
<include>**/CoreSuiteTest.java</include>
+ <include>**/BatchGraphIsolationTest.java</include>
</includes>
</configuration>
</execution>
diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/BatchGraphIsolationTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/BatchGraphIsolationTest.java
new file mode 100644
index 0000000..c222557
--- /dev/null
+++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/BatchGraphIsolationTest.java
@@ -0,0 +1,319 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.store.core;
+
+import static org.apache.hugegraph.store.constant.HugeServerTables.TABLES_MAP;
+import static org.apache.hugegraph.store.constant.HugeServerTables.VERTEX_TABLE;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.hugegraph.store.UnitTestBase;
+import org.apache.hugegraph.store.business.BusinessHandler;
+import org.apache.hugegraph.store.business.BusinessHandlerImpl;
+import org.apache.hugegraph.store.business.DataManagerImpl;
+import org.apache.hugegraph.store.business.DefaultDataMover;
+import org.apache.hugegraph.store.cmd.request.BatchPutRequest;
+import org.apache.hugegraph.store.grpc.common.Key;
+import org.apache.hugegraph.store.grpc.common.OpType;
+import org.apache.hugegraph.store.grpc.session.BatchEntry;
+import org.apache.hugegraph.store.meta.PartitionManager;
+import org.apache.hugegraph.store.options.HgStoreEngineOptions;
+import org.apache.hugegraph.store.options.RaftRocksdbOptions;
+import org.apache.hugegraph.store.pd.FakePdServiceProvider;
+import org.apache.hugegraph.store.pd.PdProvider;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.alipay.sofa.jraft.util.StorageOptionsFactory;
+import com.google.protobuf.ByteString;
+
+public class BatchGraphIsolationTest {
+
+ private static final int PARTITION_ID = 0;
+ private static final int EMPTY_PARTITION_ID = 1;
+ private static final int KEY_CODE = 0;
+ private static final int EMPTY_PARTITION_KEY_CODE = 32768;
+ private static final byte[] SHARED_KEY =
+ "shared-key".getBytes(StandardCharsets.UTF_8);
+
+ private static Path databasePath;
+ private static BusinessHandler handler;
+
+ @BeforeClass
+ public static void setup() throws IOException {
+ databasePath = Files.createTempDirectory("hugegraph-batch-graph-isolation-");
+
+ Map<String, Object> rocksdbConfig = new HashMap<>();
+ rocksdbConfig.put("rocksdb.write_buffer_size", "1048576");
+ StorageOptionsFactory.releaseAllOptions();
+ RaftRocksdbOptions.initRocksdbGlobalConfig(rocksdbConfig);
+ BusinessHandlerImpl.initRocksdb(rocksdbConfig, null);
+
+ HgStoreEngineOptions options = new HgStoreEngineOptions();
+ options.setDataPath(databasePath.toString());
+ options.setRaftPath(databasePath.toString());
+
+ HgStoreEngineOptions.FakePdOptions fakePdOptions =
+ new HgStoreEngineOptions.FakePdOptions();
+ fakePdOptions.setPartitionCount(2);
+ fakePdOptions.setPeersList("127.0.0.1");
+ fakePdOptions.setStoreList("127.0.0.1");
+ options.setFakePdOptions(fakePdOptions);
+
+ PdProvider pdProvider = new FakePdServiceProvider(fakePdOptions);
+ PartitionManager partitionManager = new PartitionManager(pdProvider, options) {
+
+ @Override
+ public String getDbDataPath(int partitionId, String dbName) {
+ return databasePath.resolve("data").toString();
+ }
+
+ @Override
+ public boolean hasPartition(String graphName, int partitionId) {
+ return partitionId == PARTITION_ID || partitionId == EMPTY_PARTITION_ID;
+ }
+
+ @Override
+ public List<Integer> getLeaderPartitionIds(String graph) {
+ return Collections.singletonList(PARTITION_ID);
+ }
+ };
+ handler = new BusinessHandlerImpl(partitionManager);
+ handler.createTable("setup", PARTITION_ID, VERTEX_TABLE);
+ }
+
+ @AfterClass
+ public static void teardown() {
+ if (handler != null) {
+ handler.closeAll();
+ }
+ if (databasePath != null) {
+ UnitTestBase.deleteDir(databasePath.toFile());
+ }
+ }
+
+ @Test(timeout = 5000L)
+ public void testFirstBatchOnEmptyPartitionCompletes() {
+ String graph = "first-batch-empty-partition";
+ byte[] value = "first-value".getBytes(StandardCharsets.UTF_8);
+
+ Assert.assertFalse(handler.existsTable(graph, EMPTY_PARTITION_ID, VERTEX_TABLE));
+ writeBatch(graph, EMPTY_PARTITION_ID, EMPTY_PARTITION_KEY_CODE,
+ OpType.OP_TYPE_PUT, value);
+ Assert.assertArrayEquals(value, readByCode(graph, EMPTY_PARTITION_KEY_CODE));
+ }
+
+ @Test(timeout = 10000L)
+ public void testTruncateWaitsForInFlightBatch() throws Exception {
+ String graph = "in-flight-batch-graph";
+ String nextGraph = "graph-after-truncate";
+ byte[] pendingValue = "pending-value".getBytes(StandardCharsets.UTF_8);
+ byte[] nextValue = "next-value".getBytes(StandardCharsets.UTF_8);
+ BusinessHandler.TxBuilder builder = handler.txBuilder(graph, PARTITION_ID);
+ BusinessHandler.Tx transaction = builder.build();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ CountDownLatch truncateStarted = new CountDownLatch(1);
+ boolean committed = false;
+
+ try {
+ put(builder, VERTEX_TABLE, pendingValue);
+ Future<?> truncate = executor.submit(() -> {
+ truncateStarted.countDown();
+ handler.truncate(graph, PARTITION_ID);
+ });
+
+ Assert.assertTrue(truncateStarted.await(1L, TimeUnit.SECONDS));
+ try {
+ truncate.get(500L, TimeUnit.MILLISECONDS);
+ Assert.fail("truncate must wait for the in-flight batch");
+ } catch (TimeoutException expected) {
+ // Expected until the transaction releases its graph ID reservation.
+ }
+
+ transaction.commit();
+ committed = true;
+ truncate.get(5L, TimeUnit.SECONDS);
+
+ writeBatch(nextGraph, OpType.OP_TYPE_PUT, nextValue);
+ Assert.assertArrayEquals(nextValue, read(nextGraph));
+ } finally {
+ if (!committed) {
+ transaction.rollback();
+ }
+ executor.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testDataManagerRollsBackFailedBatch() {
+ BusinessHandler mockHandler = Mockito.mock(BusinessHandler.class);
+ BusinessHandler.TxBuilder mockBuilder = Mockito.mock(BusinessHandler.TxBuilder.class);
+ BusinessHandler.Tx mockTransaction = Mockito.mock(BusinessHandler.Tx.class);
+ BatchPutRequest request = batchPutRequest();
+ BatchPutRequest.KV entry = request.getEntries().get(0);
+ RuntimeException failure = new RuntimeException("injected put failure");
+ Mockito.when(mockHandler.txBuilder(request.getGraphName(), request.getPartitionId()))
+ .thenReturn(mockBuilder);
+ Mockito.when(mockBuilder.build()).thenReturn(mockTransaction);
+ Mockito.doThrow(failure).when(mockBuilder)
+ .put(entry.getCode(), entry.getTable(), entry.getKey(), entry.getValue());
+ DataManagerImpl dataManager = new DataManagerImpl();
+ dataManager.setBusinessHandler(mockHandler);
+
+ assertWriteFails(failure, () -> dataManager.write(request));
+
+ Mockito.verify(mockTransaction).rollback();
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testDefaultDataMoverRollsBackFailedBatch() {
+ BusinessHandler mockHandler = Mockito.mock(BusinessHandler.class);
+ BusinessHandler.TxBuilder mockBuilder = Mockito.mock(BusinessHandler.TxBuilder.class);
+ BusinessHandler.Tx mockTransaction = Mockito.mock(BusinessHandler.Tx.class);
+ BatchPutRequest request = batchPutRequest();
+ BatchPutRequest.KV entry = request.getEntries().get(0);
+ RuntimeException failure = new RuntimeException("injected put failure");
+ Mockito.when(mockHandler.txBuilder(request.getGraphName(), request.getPartitionId()))
+ .thenReturn(mockBuilder);
+ Mockito.when(mockBuilder.build()).thenReturn(mockTransaction);
+ Mockito.doThrow(failure).when(mockBuilder)
+ .put(entry.getCode(), entry.getTable(), entry.getKey(), entry.getValue());
+ DefaultDataMover dataMover = new DefaultDataMover();
+ dataMover.setBusinessHandler(mockHandler);
+
+ assertWriteFails(failure, () -> dataMover.doWriteData(request));
+
+ Mockito.verify(mockTransaction).rollback();
+ }
+
+ @Test
+ public void testBatchPutKeepsGraphsIsolated() {
+ String graph1 = "batch-put-graph-1";
+ String graph2 = "batch-put-graph-2";
+ byte[] value1 = "graph-1-value".getBytes(StandardCharsets.UTF_8);
+ byte[] value2 = "graph-2-value".getBytes(StandardCharsets.UTF_8);
+
+ writeBatch(graph1, OpType.OP_TYPE_PUT, value1);
+ writeBatch(graph2, OpType.OP_TYPE_PUT, value2);
+
+ Assert.assertArrayEquals(value1, read(graph1));
+ Assert.assertArrayEquals(value2, read(graph2));
+
+ handler.truncate(graph2, PARTITION_ID);
+ Assert.assertNull(read(graph2));
+ Assert.assertArrayEquals(value1, read(graph1));
+ }
+
+ @Test
+ public void testBatchMergeKeepsGraphsIsolated() {
+ String graph1 = "batch-merge-graph-1";
+ String graph2 = "batch-merge-graph-2";
+
+ writeBatch(graph1, OpType.OP_TYPE_MERGE, longToBytes(10L));
+ writeBatch(graph2, OpType.OP_TYPE_MERGE, longToBytes(20L));
+
+ Assert.assertEquals(10L, bytesToLong(read(graph1)));
+ Assert.assertEquals(20L, bytesToLong(read(graph2)));
+ }
+
+ private static void writeBatch(String graph, OpType type, byte[] value) {
+ writeBatch(graph, PARTITION_ID, type, value);
+ }
+
+ private static void writeBatch(String graph, int partitionId, OpType type, byte[] value) {
+ writeBatch(graph, partitionId, KEY_CODE, type, value);
+ }
+
+ private static void writeBatch(String graph, int partitionId, int code, OpType type,
+ byte[] value) {
+ Key key = Key.newBuilder()
+ .setCode(code)
+ .setKey(ByteString.copyFrom(SHARED_KEY))
+ .build();
+ BatchEntry entry = BatchEntry.newBuilder()
+ .setOpType(type)
+ .setTable(TABLES_MAP.get(VERTEX_TABLE))
+ .setStartKey(key)
+ .setValue(ByteString.copyFrom(value))
+ .build();
+ handler.doBatch(graph, partitionId, Collections.singletonList(entry));
+ }
+
+ private static void put(BusinessHandler.TxBuilder builder, String table, byte[] value) {
+ builder.put(KEY_CODE, table, SHARED_KEY, value);
+ }
+
+ private static byte[] read(String graph) {
+ return readByCode(graph, KEY_CODE);
+ }
+
+ private static byte[] readByCode(String graph, int code) {
+ return handler.doGet(graph, code, VERTEX_TABLE, SHARED_KEY);
+ }
+
+ private static BatchPutRequest batchPutRequest() {
+ BatchPutRequest request = new BatchPutRequest();
+ request.setGraphName("failed-transfer-graph");
+ request.setPartitionId(PARTITION_ID);
+ request.getEntries()
+ .add(BatchPutRequest.KV.of(VERTEX_TABLE, KEY_CODE, SHARED_KEY,
+ "value".getBytes(StandardCharsets.UTF_8)));
+ return request;
+ }
+
+ private static void assertWriteFails(RuntimeException expected, Runnable writer) {
+ try {
+ writer.run();
+ Assert.fail("batch write must propagate its failure");
+ } catch (RuntimeException actual) {
+ Assert.assertSame(expected, actual);
+ }
+ }
+
+ private static byte[] longToBytes(long value) {
+ return ByteBuffer.allocate(Long.BYTES)
+ .order(ByteOrder.LITTLE_ENDIAN)
+ .putLong(value)
+ .array();
+ }
+
+ private static long bytesToLong(byte[] value) {
+ return ByteBuffer.wrap(value)
+ .order(ByteOrder.LITTLE_ENDIAN)
+ .getLong();
+ }
+}