├── .gitignore ├── BUILD ├── LICENSE ├── README.md ├── WORKSPACE ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── ph14 │ │ └── fdb │ │ └── zk │ │ ├── ByteUtil.java │ │ ├── FdbRequestProcessor.java │ │ ├── FdbSchemaConstants.java │ │ ├── FdbZooKeeperImpl.java │ │ ├── FdbZooKeeperServer.java │ │ ├── config │ │ └── FdbZooKeeperModule.java │ │ ├── layer │ │ ├── FdbNode.java │ │ ├── FdbNodeReader.java │ │ ├── FdbNodeWriter.java │ │ ├── FdbPath.java │ │ ├── FdbWatchManager.java │ │ ├── StatKey.java │ │ ├── changefeed │ │ │ ├── ChangefeedWatchEvent.java │ │ │ └── WatchEventChangefeed.java │ │ └── ephemeral │ │ │ └── FdbEphemeralNodeManager.java │ │ ├── ops │ │ ├── FdbCheckVersionOp.java │ │ ├── FdbCreateOp.java │ │ ├── FdbDeleteOp.java │ │ ├── FdbExistsOp.java │ │ ├── FdbGetChildrenOp.java │ │ ├── FdbGetChildrenWithStatOp.java │ │ ├── FdbGetDataOp.java │ │ ├── FdbMultiOp.java │ │ ├── FdbOp.java │ │ ├── FdbSetDataOp.java │ │ └── FdbSetWatchesOp.java │ │ └── session │ │ ├── CoordinatingClock.java │ │ ├── FdbSessionClock.java │ │ ├── FdbSessionDataPurger.java │ │ └── FdbSessionManager.java └── resources │ └── log4j.properties └── test └── java ├── com └── ph14 │ └── fdb │ └── zk │ ├── ByteUtilTest.java │ ├── FdbBaseTest.java │ ├── FdbZkServerTestUtil.java │ ├── LocalRealZooKeeperScratchTest.java │ ├── curator │ ├── LeaderCandidate.java │ └── LeaderElection.java │ ├── layer │ ├── FdbNodeSerialization.java │ └── FdbPathTest.java │ ├── ops │ ├── FdbCreateOpTest.java │ ├── FdbDeleteOpTest.java │ ├── FdbExistsOpTest.java │ ├── FdbGetChildrenWithStatOpTest.java │ ├── FdbGetDataOpTest.java │ ├── FdbMultiOpTest.java │ ├── FdbSetDataOpTest.java │ └── ThrowingWatchManager.java │ ├── session │ ├── CoordinatingClockTest.java │ └── FdbSessionManagerTest.java │ └── watches │ └── FdbWatchLocalIntegrationTests.java └── org └── apache └── zookeeper └── server └── MockFdbServerCnxn.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .idea 26 | *.iml 27 | target/ 28 | 29 | # Bazel 30 | bazel-* -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | 2 | # NOTE: `bazel query @maven//:all --output=build` 3 | 4 | java_library( 5 | name = "fdb_zk", 6 | srcs = glob(["src/main/java/**/*.java"]), 7 | resources = glob(["src/main/resources/**"]), 8 | deps = [ 9 | "@maven//:org_foundationdb_fdb_java", 10 | "@maven//:org_apache_zookeeper_zookeeper", 11 | "@maven//:com_google_inject_guice", 12 | "@maven//:com_google_guava_guava", 13 | "@maven//:com_hubspot_algebra", 14 | "@maven//:org_slf4j_slf4j_api", 15 | "@maven//:org_slf4j_slf4j_log4j12", 16 | "@maven//:io_netty_netty", 17 | ], 18 | ) 19 | 20 | java_test( 21 | name = "fdb_zk_test", 22 | srcs = glob(["src/test/java/**/*.java"]), 23 | test_class = "com.ph14.fdb.zk.FdbTest", 24 | size = "small", 25 | runtime_deps = [ 26 | 27 | ], 28 | deps = [ 29 | "@maven//:junit_junit", 30 | "@maven//:org_assertj_assertj_core", 31 | "@maven//:com_google_guava_guava", 32 | "@maven//:org_foundationdb_fdb_java", 33 | "@maven//:org_apache_zookeeper_zookeeper", 34 | "@maven//:com_hubspot_algebra", 35 | "@maven//:org_slf4j_slf4j_api", 36 | ":fdb_zk", 37 | ], 38 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Paul Hemberger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## fdb-zk 2 | 3 | `fdb-zk` is a FoundationDB layer that mimics the behavior of Zookeeper. It is installed as a local service to an application, and replaces connections to and the operation of a ZooKeeper cluster. 4 | 5 | While the core operations are implemented, `fdb-zk` has not been vetted for proper production use. 6 | 7 | ### Talk & Slides 8 | 9 | Learn about how the layer works in greater detail: 10 | 11 | * [In video format](https://www.youtube.com/watch?v=3FYpf1QMPgQ) 12 | * [In slidedeck format](https://static.sched.com/hosted_files/foundationdbsummit2019/86/zookeeper_layer.pdf) 13 | 14 | #### Architecture 15 | 16 | Similar to the [FoundationDB Document Layer](https://foundationdb.github.io/fdb-document-layer/), `fdb-zk` is hosted locally and translates requests for the target service into FoundationDB transactions. 17 | 18 | Applications can continue to use their preferred `Zookeeper` clients: 19 | 20 | ``` 21 | ┌──────────────────────┐ ┌──────────────────────┐ 22 | │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ 23 | │ │ Application │ │ │ │ Application │ │ 24 | │ └──────────────────┘ │ │ └──────────────────┘ │ 25 | │ │ │ │ │ │ 26 | │ │ │ │ │ │ 27 | │ ZooKeeper │ │ ZooKeeper │ 28 | │ protocol │ │ protocol │ 29 | │ │ │ │ │ │ 30 | │ │ │ │ │ │ 31 | │ ▼ │ │ ▼ │ 32 | │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ 33 | │ │ fdb-zk service │ │ │ │ fdb-zk service │ │ 34 | │ └──────────────────┘ │ │ └──────────────────┘ │ 35 | └──────────────────────┘ └──────────────────────┘ 36 | │ │ 37 | FDB ops FDB ops 38 | │ │ 39 | ▼ ▼ 40 | ┌───────────────────────────────────────────────────┐ 41 | │ FoundationDB │ 42 | └───────────────────────────────────────────────────┘ 43 | ``` 44 | 45 | ### Features 46 | 47 | `fdb-zk` implements the core Zookeeper 3.4.6 API: 48 | 49 | * `create` 50 | * `exists` 51 | * `delete` 52 | * `getData` 53 | * `setData` 54 | * `getChildren` 55 | * watches 56 | * session management 57 | 58 | It partially implements: 59 | 60 | * `multi` transactions (reads are fine, but there are no read-your-writes) 61 | 62 | It does not yet implement: 63 | 64 | * `getACL/setACL` 65 | * quotas 66 | 67 | ### Initial Design Discussion 68 | 69 | https://forums.foundationdb.org/t/fdb-zk-rough-cut-of-zookeeper-api-layer/1278/ 70 | 71 | ### Building with Bazel 72 | 73 | * Compiling: `bazel build //:fdb_zk` 74 | * Testing: `bazel test //:fdb_zk_test` 75 | * Dependencies: `bazel query @maven//:all --output=build` 76 | 77 | ### License 78 | 79 | `fdb-zk` is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details. 80 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "fdb_zk") 2 | 3 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 4 | 5 | RULES_JVM_EXTERNAL_TAG = "2.1" 6 | RULES_JVM_EXTERNAL_SHA = "515ee5265387b88e4547b34a57393d2bcb1101314bcc5360ec7a482792556f42" 7 | 8 | http_archive( 9 | name = "rules_jvm_external", 10 | strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG, 11 | sha256 = RULES_JVM_EXTERNAL_SHA, 12 | url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG, 13 | ) 14 | 15 | load("@rules_jvm_external//:defs.bzl", "maven_install") 16 | 17 | maven_install( 18 | artifacts = [ 19 | "com.google.guava:guava:27.0-jre", 20 | "org.foundationdb:fdb-java:6.0.15", 21 | "org.apache.zookeeper:zookeeper:3.4.6", 22 | "com.hubspot:algebra:1.2", 23 | "com.google.inject:guice:4.1.0", 24 | "org.assertj:assertj-core:3.5.2", 25 | ], 26 | repositories = [ 27 | "https://jcenter.bintray.com/", 28 | "https://maven.google.com", 29 | "https://repo1.maven.org/maven2", 30 | ], 31 | ) -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.ph14 8 | fdb-zk 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | org.apache.maven.plugins 14 | maven-compiler-plugin 15 | 16 | 8 17 | 8 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | com.google.guava 26 | guava 27 | 27.0-jre 28 | 29 | 30 | org.foundationdb 31 | fdb-java 32 | 6.1.8 33 | 34 | 35 | org.apache.zookeeper 36 | zookeeper 37 | 3.4.6 38 | 39 | 40 | com.hubspot 41 | algebra 42 | 1.2 43 | 44 | 45 | com.google.inject 46 | guice 47 | 4.1.0 48 | 49 | 50 | org.apache.curator 51 | curator-client 52 | 4.2.0 53 | 54 | 55 | org.apache.curator 56 | curator-recipes 57 | 4.2.0 58 | 59 | 60 | 61 | junit 62 | junit 63 | 4.12 64 | test 65 | 66 | 67 | org.assertj 68 | assertj-core 69 | 3.5.2 70 | test 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ByteUtil.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class ByteUtil { 7 | 8 | public static List divideByteArray(byte[] source, int chunkSize) { 9 | int numberOfChunks = (int) Math.ceil(source.length / (double) chunkSize); 10 | int remainingBytes = source.length; 11 | 12 | List chunks = new ArrayList<>(numberOfChunks); 13 | 14 | int chunkIndex = 0; 15 | byte[] chunk = new byte[Integer.min(remainingBytes, chunkSize)]; 16 | 17 | for (; remainingBytes > 0; remainingBytes--) { 18 | if (chunkIndex == chunkSize) { 19 | chunks.add(chunk); 20 | chunk = new byte[Integer.min(remainingBytes, chunkSize)]; 21 | chunkIndex = 0; 22 | } 23 | 24 | chunk[chunkIndex] = source[source.length - remainingBytes]; 25 | chunkIndex++; 26 | } 27 | 28 | chunks.add(chunk); 29 | 30 | return chunks; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/FdbRequestProcessor.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk; 2 | 3 | import java.io.IOException; 4 | 5 | import org.apache.jute.Record; 6 | import org.apache.zookeeper.KeeperException; 7 | import org.apache.zookeeper.KeeperException.Code; 8 | import org.apache.zookeeper.ZooDefs.OpCode; 9 | import org.apache.zookeeper.proto.ReplyHeader; 10 | import org.apache.zookeeper.server.Request; 11 | import org.apache.zookeeper.server.RequestProcessor; 12 | import org.apache.zookeeper.server.ZooKeeperServer; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import com.google.common.base.Preconditions; 17 | import com.hubspot.algebra.Result; 18 | 19 | public class FdbRequestProcessor implements RequestProcessor { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(FdbRequestProcessor.class); 22 | 23 | private final FdbZooKeeperImpl fdbZooKeeper; 24 | private final ZooKeeperServer zooKeeperServer; 25 | private final RequestProcessor defaultRequestProcessor; 26 | 27 | public FdbRequestProcessor(RequestProcessor defaultRequestProcessor, 28 | ZooKeeperServer zooKeeperServer, 29 | FdbZooKeeperImpl fdbZooKeeper) { 30 | this.defaultRequestProcessor = defaultRequestProcessor; 31 | this.zooKeeperServer = zooKeeperServer; 32 | this.fdbZooKeeper = fdbZooKeeper; 33 | } 34 | 35 | @Override 36 | public void processRequest(Request request) { 37 | LOG.info("Received request in Fdb Request Processor: {}", request); 38 | 39 | Preconditions.checkState( 40 | fdbZooKeeper.handlesRequest(request), 41 | String.format("given unprocessable request type %s", request.type)); 42 | 43 | try { 44 | sendResponse(request, fdbZooKeeper.handle(request)); 45 | } catch (IOException e) { 46 | LOG.error("Failed to process request {}", request, e); 47 | } 48 | } 49 | 50 | private void sendResponse(Request request, 51 | Result fdbResult) { 52 | if (request.type == OpCode.createSession) { 53 | zooKeeperServer.finishSessionInit(request.cnxn, true); 54 | return; 55 | } 56 | 57 | Record result = null; 58 | int errorCode = Code.OK.intValue(); 59 | 60 | if (fdbResult.isOk()) { 61 | Object o = fdbResult.unwrapOrElseThrow(); 62 | 63 | if (o instanceof Record) { 64 | result = (Record) o; 65 | } 66 | 67 | // note: it's OK for result to be null in the case of some operations like setWatches 68 | 69 | errorCode = Code.OK.intValue(); 70 | } else if (fdbResult.isErr()) { 71 | LOG.error("Error: ", fdbResult.unwrapErrOrElseThrow()); 72 | result = null; 73 | errorCode = fdbResult.unwrapErrOrElseThrow().code().intValue(); 74 | } else { 75 | LOG.error("What is going on send help. Request: {}", request); 76 | } 77 | 78 | ReplyHeader hdr = new ReplyHeader(request.cxid, System.currentTimeMillis(), errorCode); 79 | 80 | try { 81 | LOG.debug("Returning: {}", result); 82 | request.cnxn.sendResponse(hdr, result, "response"); 83 | } catch (IOException e) { 84 | LOG.error("FIXMSG",e); 85 | } 86 | } 87 | 88 | public void shutdown() { 89 | 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/FdbSchemaConstants.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk; 2 | 3 | public class FdbSchemaConstants { 4 | 5 | public static final int FDB_MAX_VALUE_SIZE = 100_000; 6 | public static final int ZK_MAX_DATA_LENGTH = 1_048_576; 7 | 8 | public static final byte[] DATA_KEY = "d".getBytes(); 9 | public static final byte[] ACL_KEY = "a".getBytes(); 10 | public static final byte[] STAT_KEY = "s".getBytes(); 11 | 12 | public static final byte[] NODE_CREATED_WATCH_KEY = "w".getBytes(); 13 | public static final byte[] CHILD_CREATED_WATCH_KEY = "c".getBytes(); 14 | public static final byte[] NODE_DATA_UPDATED_KEY = "u".getBytes(); 15 | 16 | // Creation ZXID. Use Versionstamp at creation time 17 | public static final byte[] CZXID_KEY = "sc".getBytes(); 18 | // Modified ZXID. Use Versionstamp at creation + updates 19 | public static final byte[] MZXID_KEY = "sm".getBytes(); 20 | // Creation timestamp 21 | public static final byte[] CTIME_KEY = "sct".getBytes(); 22 | // Modified timestamp 23 | public static final byte[] MTIME_KEY = "smt".getBytes(); 24 | // # of changes to this node 25 | public static final byte[] VERSION_KEY = "sv".getBytes(); 26 | // # of changes to this node's children 27 | public static final byte[] CVERSION_KEY = "svc".getBytes(); 28 | // # of changes to this node's ACL 29 | public static final byte[] AVERSION_KEY = "sva".getBytes(); 30 | // Ephemeral node owner 31 | public static final byte[] EPHEMERAL_OWNER_KEY = "se".getBytes(); 32 | // Data length 33 | public static final byte[] DATA_LENGTH_KEY = "sd".getBytes(); 34 | // Number of children 35 | public static final byte[] NUM_CHILDREN_KEY = "snc".getBytes(); 36 | 37 | public static final byte[] EMPTY_BYTES = new byte[0]; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/FdbZooKeeperImpl.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk; 2 | 3 | import java.io.IOException; 4 | import java.util.Arrays; 5 | import java.util.Set; 6 | 7 | import org.apache.zookeeper.KeeperException; 8 | import org.apache.zookeeper.KeeperException.SessionExpiredException; 9 | import org.apache.zookeeper.KeeperException.SessionMovedException; 10 | import org.apache.zookeeper.MultiTransactionRecord; 11 | import org.apache.zookeeper.ZooDefs.OpCode; 12 | import org.apache.zookeeper.proto.CheckVersionRequest; 13 | import org.apache.zookeeper.proto.CreateRequest; 14 | import org.apache.zookeeper.proto.DeleteRequest; 15 | import org.apache.zookeeper.proto.ExistsRequest; 16 | import org.apache.zookeeper.proto.GetChildren2Request; 17 | import org.apache.zookeeper.proto.GetChildrenRequest; 18 | import org.apache.zookeeper.proto.GetDataRequest; 19 | import org.apache.zookeeper.proto.SetACLRequest; 20 | import org.apache.zookeeper.proto.SetDataRequest; 21 | import org.apache.zookeeper.proto.SetWatches; 22 | import org.apache.zookeeper.proto.SyncRequest; 23 | import org.apache.zookeeper.proto.SyncResponse; 24 | import org.apache.zookeeper.server.ByteBufferInputStream; 25 | import org.apache.zookeeper.server.Request; 26 | import org.apache.zookeeper.txn.CreateSessionTxn; 27 | 28 | import com.apple.foundationdb.Database; 29 | import com.google.common.base.Preconditions; 30 | import com.google.common.collect.ImmutableSet; 31 | import com.google.inject.Inject; 32 | import com.hubspot.algebra.Result; 33 | import com.ph14.fdb.zk.ops.FdbCheckVersionOp; 34 | import com.ph14.fdb.zk.ops.FdbCreateOp; 35 | import com.ph14.fdb.zk.ops.FdbDeleteOp; 36 | import com.ph14.fdb.zk.ops.FdbExistsOp; 37 | import com.ph14.fdb.zk.ops.FdbGetChildrenOp; 38 | import com.ph14.fdb.zk.ops.FdbGetChildrenWithStatOp; 39 | import com.ph14.fdb.zk.ops.FdbGetDataOp; 40 | import com.ph14.fdb.zk.ops.FdbMultiOp; 41 | import com.ph14.fdb.zk.ops.FdbSetDataOp; 42 | import com.ph14.fdb.zk.ops.FdbSetWatchesOp; 43 | import com.ph14.fdb.zk.session.FdbSessionManager; 44 | 45 | public class FdbZooKeeperImpl { 46 | 47 | private static final Set OPS_WITHOUT_SESSION_CHECK = ImmutableSet.builder() 48 | .add(OpCode.createSession) 49 | .add(OpCode.closeSession) 50 | .build(); 51 | 52 | private static final Set FDB_SUPPORTED_OPCODES = ImmutableSet.builder() 53 | .addAll( 54 | Arrays.asList( 55 | OpCode.create, 56 | OpCode.delete, 57 | OpCode.setData, 58 | OpCode.setACL, 59 | OpCode.multi, 60 | OpCode.exists, 61 | OpCode.getData, 62 | OpCode.getACL, 63 | OpCode.getChildren, 64 | OpCode.getChildren2, // includes stat of node 65 | OpCode.setWatches, 66 | OpCode.multi, 67 | OpCode.check, 68 | 69 | OpCode.sync, 70 | OpCode.ping, 71 | 72 | OpCode.createSession, 73 | OpCode.closeSession 74 | ) 75 | ) 76 | .build(); 77 | 78 | private final Database fdb; 79 | private final FdbCreateOp fdbCreateOp; 80 | private final FdbCheckVersionOp fdbCheckVersionOp; 81 | private final FdbExistsOp fdbExistsOp; 82 | private final FdbGetDataOp fdbGetDataOp; 83 | private final FdbSetDataOp fdbSetDataOp; 84 | private final FdbGetChildrenOp fdbGetChildrenOp; 85 | private final FdbGetChildrenWithStatOp fdbGetChildrenWithStatOp; 86 | private final FdbDeleteOp fdbDeleteOp; 87 | private final FdbSetWatchesOp fdbSetWatchesOp; 88 | private final FdbMultiOp fdbMultiOp; 89 | 90 | private final FdbSessionManager fdbSessionManager; 91 | 92 | @Inject 93 | public FdbZooKeeperImpl(Database fdb, 94 | FdbSessionManager fdbSessionManager, 95 | FdbCreateOp fdbCreateOp, 96 | FdbCheckVersionOp fdbCheckVersionOp, 97 | FdbExistsOp fdbExistsOp, 98 | FdbGetDataOp fdbGetDataOp, 99 | FdbSetDataOp fdbSetDataOp, 100 | FdbGetChildrenOp fdbGetChildrenOp, 101 | FdbGetChildrenWithStatOp fdbGetChildrenWithStatOp, 102 | FdbDeleteOp fdbDeleteOp, 103 | FdbSetWatchesOp fdbSetWatchesOp, 104 | FdbMultiOp fdbMultiOp) { 105 | this.fdb = fdb; 106 | this.fdbSessionManager = fdbSessionManager; 107 | this.fdbCreateOp = fdbCreateOp; 108 | this.fdbCheckVersionOp = fdbCheckVersionOp; 109 | this.fdbExistsOp = fdbExistsOp; 110 | this.fdbGetDataOp = fdbGetDataOp; 111 | this.fdbSetDataOp = fdbSetDataOp; 112 | this.fdbGetChildrenOp = fdbGetChildrenOp; 113 | this.fdbGetChildrenWithStatOp = fdbGetChildrenWithStatOp; 114 | this.fdbDeleteOp = fdbDeleteOp; 115 | this.fdbSetWatchesOp = fdbSetWatchesOp; 116 | this.fdbMultiOp = fdbMultiOp; 117 | } 118 | 119 | public boolean handlesRequest(Request request) { 120 | return FDB_SUPPORTED_OPCODES.contains(request.type); 121 | } 122 | 123 | public Result handle(Request request) throws IOException { 124 | Preconditions.checkArgument(handlesRequest(request), "does not handle request: " + request); 125 | 126 | if (!OPS_WITHOUT_SESSION_CHECK.contains(request.type)) { 127 | try { 128 | fdbSessionManager.checkSession(request.sessionId, null); 129 | } catch (SessionExpiredException | SessionMovedException e) { 130 | return Result.err(e); 131 | } 132 | } 133 | 134 | switch (request.type) { 135 | case OpCode.create: 136 | CreateRequest create2Request = new CreateRequest(); 137 | ByteBufferInputStream.byteBuffer2Record(request.request, create2Request); 138 | return fdb.run(tr -> fdbCreateOp.execute(request, tr, create2Request)).join(); 139 | 140 | case OpCode.exists: 141 | ExistsRequest existsRequest = new ExistsRequest(); 142 | ByteBufferInputStream.byteBuffer2Record(request.request, existsRequest); 143 | return fdb.run(tr -> fdbExistsOp.execute(request, tr, existsRequest)).join(); 144 | 145 | case OpCode.delete: 146 | DeleteRequest deleteRequest = new DeleteRequest(); 147 | ByteBufferInputStream.byteBuffer2Record(request.request, deleteRequest); 148 | return fdb.run(tr -> fdbDeleteOp.execute(request, tr, deleteRequest)) 149 | .thenApply(r -> r.mapOk(success -> deleteRequest)) 150 | .join(); 151 | 152 | case OpCode.getData: 153 | GetDataRequest getDataRequest = new GetDataRequest(); 154 | ByteBufferInputStream.byteBuffer2Record(request.request, getDataRequest); 155 | return fdb.run(tr -> fdbGetDataOp.execute(request, tr, getDataRequest)).join(); 156 | 157 | case OpCode.getChildren: 158 | GetChildrenRequest getChildrenRequest = new GetChildrenRequest(); 159 | ByteBufferInputStream.byteBuffer2Record(request.request, getChildrenRequest); 160 | return fdb.run(tr -> fdbGetChildrenOp.execute(request, tr, getChildrenRequest)).join(); 161 | 162 | case OpCode.getChildren2: 163 | GetChildren2Request getChildren2Request = new GetChildren2Request(); 164 | ByteBufferInputStream.byteBuffer2Record(request.request, getChildren2Request); 165 | return fdb.run(tr -> fdbGetChildrenWithStatOp.execute(request, tr, getChildren2Request)).join(); 166 | 167 | case OpCode.setData: 168 | SetDataRequest setDataRequest = new SetDataRequest(); 169 | ByteBufferInputStream.byteBuffer2Record(request.request, setDataRequest); 170 | return fdb.run(tr -> fdbSetDataOp.execute(request, tr, setDataRequest)).join(); 171 | 172 | case OpCode.check: 173 | CheckVersionRequest checkVersionRequest = new CheckVersionRequest(); 174 | ByteBufferInputStream.byteBuffer2Record(request.request, checkVersionRequest); 175 | return fdb.run(tr -> fdbCheckVersionOp.execute(request, tr, checkVersionRequest)).join(); 176 | 177 | case OpCode.setACL: 178 | SetACLRequest setACLRequest = new SetACLRequest(); 179 | ByteBufferInputStream.byteBuffer2Record(request.request, setACLRequest); 180 | throw new UnsupportedOperationException("not there yet"); 181 | 182 | case OpCode.setWatches: 183 | SetWatches setWatches = new SetWatches(); 184 | ByteBufferInputStream.byteBuffer2Record(request.request, setWatches); 185 | return fdb.run(tr -> fdbSetWatchesOp.execute(request, tr, setWatches)).join(); 186 | 187 | case OpCode.createSession: 188 | request.request.rewind(); 189 | int to = request.request.getInt(); 190 | request.txn = new CreateSessionTxn(to); 191 | fdbSessionManager.addSession(request.sessionId, to); 192 | return Result.ok(true); 193 | 194 | case OpCode.closeSession: 195 | fdbSessionManager.setSessionClosing(request.sessionId); 196 | return Result.ok(true); 197 | 198 | case OpCode.multi: 199 | MultiTransactionRecord multiTransactionRecord = new MultiTransactionRecord(); 200 | ByteBufferInputStream.byteBuffer2Record(request.request, multiTransactionRecord); 201 | return Result.ok(fdbMultiOp.execute(request, multiTransactionRecord)); 202 | 203 | case OpCode.sync: // no-op, fdb won't return stale reads 204 | SyncRequest syncRequest = new SyncRequest(); 205 | ByteBufferInputStream.byteBuffer2Record(request.request, syncRequest); 206 | return Result.ok(new SyncResponse(syncRequest.getPath())); 207 | 208 | case OpCode.ping: 209 | return Result.ok(true); 210 | } 211 | 212 | return Result.err(new KeeperException.BadArgumentsException()); 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/FdbZooKeeperServer.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.time.Clock; 6 | import java.util.Collections; 7 | import java.util.OptionalLong; 8 | 9 | import org.apache.zookeeper.ZooDefs.Ids; 10 | import org.apache.zookeeper.server.SessionTracker.Session; 11 | import org.apache.zookeeper.server.ZooKeeperServer; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import com.apple.foundationdb.Database; 16 | import com.apple.foundationdb.FDB; 17 | import com.apple.foundationdb.directory.DirectoryLayer; 18 | import com.apple.foundationdb.directory.DirectorySubspace; 19 | import com.google.inject.Guice; 20 | import com.google.inject.Injector; 21 | import com.ph14.fdb.zk.config.FdbZooKeeperModule; 22 | import com.ph14.fdb.zk.layer.FdbNode; 23 | import com.ph14.fdb.zk.layer.FdbNodeWriter; 24 | import com.ph14.fdb.zk.layer.FdbPath; 25 | import com.ph14.fdb.zk.session.FdbSessionClock; 26 | import com.ph14.fdb.zk.session.FdbSessionDataPurger; 27 | import com.ph14.fdb.zk.session.FdbSessionManager; 28 | 29 | public class FdbZooKeeperServer extends ZooKeeperServer { 30 | 31 | private static final Logger LOG = LoggerFactory.getLogger(FdbZooKeeperServer.class); 32 | 33 | public FdbZooKeeperServer(int tickTime) throws IOException { 34 | super(null, null, tickTime); 35 | } 36 | 37 | @Override 38 | public void startup() { 39 | System.out.println("Starting up the server"); 40 | 41 | try (Database fdb = FDB.selectAPIVersion(600).open()) { 42 | fdb.run(tr -> { 43 | boolean rootNodeAlreadyExists = DirectoryLayer.getDefault().exists(tr, Collections.singletonList(FdbPath.ROOT_PATH)).join(); 44 | 45 | if (!rootNodeAlreadyExists) { 46 | LOG.info("Creating root path '/'"); 47 | DirectorySubspace rootSubspace = DirectoryLayer.getDefault().create(tr, Collections.singletonList(FdbPath.ROOT_PATH)).join(); 48 | new FdbNodeWriter().createNewNode(tr, rootSubspace, new FdbNode("/", null, new byte[0], Ids.OPEN_ACL_UNSAFE)); 49 | } 50 | 51 | return null; 52 | }); 53 | } 54 | 55 | getZKDatabase().setlastProcessedZxid(Long.MAX_VALUE); 56 | 57 | super.startup(); 58 | } 59 | 60 | @Override 61 | protected void createSessionTracker() { 62 | sessionTracker = new FdbSessionManager(FDB.selectAPIVersion(600).open(), Clock.systemUTC(), OptionalLong.empty()); 63 | } 64 | 65 | @Override 66 | protected void startSessionTracker() { 67 | } 68 | 69 | @Override 70 | public void expire(Session session) { 71 | LOG.debug("Expiring session: {}", session); 72 | super.expire(session); 73 | } 74 | 75 | @Override 76 | public void loadData() { 77 | } 78 | 79 | @Override 80 | protected void killSession(long sessionId, long zxid) { 81 | } 82 | 83 | @Override 84 | protected void setupRequestProcessors() { 85 | super.setupRequestProcessors(); 86 | 87 | Injector injector = Guice.createInjector(new FdbZooKeeperModule()); 88 | 89 | FdbZooKeeperImpl fdbZooKeeper = injector.getInstance(FdbZooKeeperImpl.class); 90 | 91 | FdbSessionClock fdbSessionClock = new FdbSessionClock( 92 | injector.getInstance(Database.class), 93 | Clock.systemUTC(), 94 | OptionalLong.empty(), 95 | (FdbSessionManager) sessionTracker, 96 | injector.getInstance(FdbSessionDataPurger.class)); 97 | 98 | // if this is the only node connecting, we want to clear ephemerals before allowing requests through 99 | fdbSessionClock.runOnce(); 100 | fdbSessionClock.run(); 101 | 102 | this.firstProcessor = new FdbRequestProcessor(null, this, fdbZooKeeper); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/config/FdbZooKeeperModule.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.config; 2 | 3 | import java.time.Clock; 4 | import java.util.OptionalLong; 5 | 6 | import com.apple.foundationdb.Database; 7 | import com.apple.foundationdb.FDB; 8 | import com.apple.foundationdb.directory.DirectorySubspace; 9 | import com.google.inject.AbstractModule; 10 | import com.google.inject.Provides; 11 | import com.google.inject.name.Named; 12 | 13 | public class FdbZooKeeperModule extends AbstractModule { 14 | 15 | public static final String EPHEMERAL_NODE_DIRECTORY = "ephemeral-node-directory"; 16 | public static final String SESSION_DIRECTORY = "fdb-zk-sessions-by-id"; 17 | public static final String SESSION_TIMEOUT_DIRECTORY = "fdb-zk-sessions-by-timeout"; 18 | 19 | @Override 20 | protected void configure() { 21 | } 22 | 23 | @Provides 24 | Database getFdbDatabase() { 25 | return FDB.selectAPIVersion(600).open(); 26 | } 27 | 28 | @Provides 29 | Clock getClock() { 30 | return Clock.systemUTC(); 31 | } 32 | 33 | @Provides 34 | @Named("serverTickMillis") 35 | OptionalLong getServerTickMillis() { 36 | return OptionalLong.empty(); 37 | } 38 | 39 | // TODO: @Inject the directories so the threads don't compete 40 | @Provides 41 | @Named(EPHEMERAL_NODE_DIRECTORY) 42 | DirectorySubspace getEphemeralNodeDirectory(Database fdb) { 43 | return null; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/layer/FdbNode.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.layer; 2 | 3 | import java.util.List; 4 | import java.util.Objects; 5 | import java.util.Optional; 6 | 7 | import org.apache.zookeeper.data.ACL; 8 | import org.apache.zookeeper.data.Stat; 9 | 10 | import com.google.common.base.MoreObjects; 11 | 12 | public class FdbNode { 13 | 14 | private final String zkPath; 15 | private final Stat stat; 16 | private final byte[] data; 17 | private final List acls; 18 | private final Optional ephemeralSessionId; 19 | 20 | public FdbNode(String zkPath, Stat stat, byte[] data, List acls) { 21 | this(zkPath, stat, data, acls, Optional.empty()); 22 | } 23 | 24 | public FdbNode(String zkPath, Stat stat, byte[] data, List acls, Optional ephemeralSessionId) { 25 | this.zkPath = zkPath; 26 | this.stat = stat; 27 | this.data = data; 28 | this.acls = acls; 29 | this.ephemeralSessionId = ephemeralSessionId; 30 | } 31 | 32 | public String getZkPath() { 33 | return zkPath; 34 | } 35 | 36 | public List getFdbPath() { 37 | return FdbPath.toFdbPath(zkPath); 38 | } 39 | 40 | public Stat getStat() { 41 | return stat; 42 | } 43 | 44 | public byte[] getData() { 45 | return data; 46 | } 47 | 48 | public List getAcls() { 49 | return acls; 50 | } 51 | 52 | public Optional getEphemeralSessionId() { 53 | return ephemeralSessionId; 54 | } 55 | 56 | @Override 57 | public boolean equals(Object obj) { 58 | if (this == obj) { 59 | return true; 60 | } 61 | if (obj instanceof FdbNode) { 62 | final FdbNode that = (FdbNode) obj; 63 | return Objects.equals(this.getZkPath(), that.getZkPath()) 64 | && Objects.equals(this.getStat(), that.getStat()) 65 | && Objects.equals(this.getData(), that.getData()) 66 | && Objects.equals(this.getAcls(), that.getAcls()) 67 | && Objects.equals(this.getEphemeralSessionId(), that.getEphemeralSessionId()); 68 | } 69 | return false; 70 | } 71 | 72 | @Override 73 | public int hashCode() { 74 | return Objects.hash(getZkPath(), getStat(), getData(), getAcls(), getEphemeralSessionId()); 75 | } 76 | 77 | @Override 78 | public String toString() { 79 | return MoreObjects.toStringHelper(this) 80 | .add("zkPath", zkPath) 81 | .add("stat", stat) 82 | .add("data", data) 83 | .add("acls", acls) 84 | .add("ephemeralSessionId", ephemeralSessionId) 85 | .toString(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/layer/FdbNodeReader.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.layer; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | import org.apache.zookeeper.data.ACL; 9 | import org.apache.zookeeper.data.Stat; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import com.apple.foundationdb.KeyValue; 14 | import com.apple.foundationdb.Transaction; 15 | import com.apple.foundationdb.async.AsyncUtil; 16 | import com.apple.foundationdb.directory.DirectoryLayer; 17 | import com.apple.foundationdb.directory.DirectorySubspace; 18 | import com.apple.foundationdb.subspace.Subspace; 19 | import com.apple.foundationdb.tuple.ByteArrayUtil; 20 | import com.google.common.collect.ArrayListMultimap; 21 | import com.google.common.collect.ImmutableList; 22 | import com.google.common.collect.ListMultimap; 23 | import com.google.common.io.ByteStreams; 24 | import com.google.common.primitives.Ints; 25 | import com.google.common.primitives.Longs; 26 | import com.google.inject.Inject; 27 | import com.ph14.fdb.zk.FdbSchemaConstants; 28 | 29 | public class FdbNodeReader { 30 | 31 | private static final Logger LOG = LoggerFactory.getLogger(FdbNodeReader.class); 32 | 33 | @Inject 34 | public FdbNodeReader() { 35 | } 36 | 37 | public CompletableFuture getNodeDirectory(Transaction transaction, String zkPath) { 38 | return DirectoryLayer.getDefault().open(transaction, FdbPath.toFdbPath(zkPath)); 39 | } 40 | 41 | public CompletableFuture getNode(DirectorySubspace nodeSubspace, Transaction transaction) { 42 | return transaction.getRange(nodeSubspace.range()).asList() 43 | .thenApply(kvs -> getNode(nodeSubspace, kvs)); 44 | } 45 | 46 | public FdbNode getNode(DirectorySubspace nodeSubspace, List keyValues) { 47 | ListMultimap keyValuesByPrefix = ArrayListMultimap.create(); 48 | 49 | byte[] statPrefix = nodeSubspace.get(FdbSchemaConstants.STAT_KEY).pack(); 50 | byte[] aclPrefix = nodeSubspace.get(FdbSchemaConstants.ACL_KEY).pack(); 51 | byte[] dataPrefix = nodeSubspace.get(FdbSchemaConstants.DATA_KEY).pack(); 52 | 53 | for (KeyValue keyValue : keyValues) { 54 | if (ByteArrayUtil.startsWith(keyValue.getKey(), statPrefix)) { 55 | keyValuesByPrefix.put(FdbSchemaConstants.STAT_KEY, keyValue); 56 | } else if (ByteArrayUtil.startsWith(keyValue.getKey(), aclPrefix)) { 57 | keyValuesByPrefix.put(FdbSchemaConstants.ACL_KEY, keyValue); 58 | } else if (ByteArrayUtil.startsWith(keyValue.getKey(), dataPrefix)) { 59 | keyValuesByPrefix.put(FdbSchemaConstants.DATA_KEY, keyValue); 60 | } 61 | } 62 | 63 | return new FdbNode( 64 | FdbPath.toZkPath(nodeSubspace.getPath()), 65 | getNodeStat(nodeSubspace, keyValuesByPrefix.get(FdbSchemaConstants.STAT_KEY)), 66 | getData(keyValuesByPrefix.get(FdbSchemaConstants.DATA_KEY)), 67 | getAcls(keyValuesByPrefix.get(FdbSchemaConstants.ACL_KEY))); 68 | } 69 | 70 | public CompletableFuture getNodeStat(Subspace nodeSubspace, Transaction transaction) { 71 | return getNodeStat( 72 | nodeSubspace, 73 | transaction.getRange(nodeSubspace.get(FdbSchemaConstants.STAT_KEY).range()).asList()); 74 | } 75 | 76 | public CompletableFuture getNodeStat(Subspace nodeSubspace, CompletableFuture> keyValues) { 77 | return keyValues.thenApply(kvs -> getNodeStat(nodeSubspace, kvs)); 78 | } 79 | 80 | public CompletableFuture getNodeStat(Subspace nodeSubspace, Transaction transaction, StatKey ... statKeys) { 81 | Subspace statSubspace = nodeSubspace.get(FdbSchemaConstants.STAT_KEY); 82 | 83 | List> futures = new ArrayList<>(statKeys.length); 84 | 85 | for (StatKey statKey : statKeys) { 86 | byte[] key = statKey.toKey(statSubspace); 87 | futures.add(transaction.get(key).thenApply(value -> new KeyValue(key, value))); 88 | } 89 | 90 | return AsyncUtil.getAll(futures).thenApply(kvs -> getNodeStat(nodeSubspace, kvs)); 91 | } 92 | 93 | private byte[] getData(List keyValues) { 94 | return ByteArrayUtil.join( 95 | new byte[0], 96 | keyValues.stream() 97 | .map(KeyValue::getValue) 98 | .collect(ImmutableList.toImmutableList())); 99 | } 100 | 101 | private List getAcls(List keyValues) { 102 | return keyValues.stream() 103 | .map(kv -> { 104 | final ACL acl = new ACL(); 105 | 106 | try { 107 | acl.readFields(ByteStreams.newDataInput(kv.getValue())); 108 | } catch (IOException e) { 109 | LOG.error("Unknown stat bytes", e); 110 | } 111 | 112 | return acl; 113 | }) 114 | .collect(ImmutableList.toImmutableList()); 115 | } 116 | 117 | private Stat getNodeStat(Subspace nodeSubspace, List keyValues) { 118 | Subspace nodeStatSubspace = nodeSubspace.get(FdbSchemaConstants.STAT_KEY); 119 | return readStatFromKeyValues(nodeStatSubspace, keyValues); 120 | } 121 | 122 | private Stat readStatFromKeyValues(Subspace nodeStatSubspace, List keyValues) { 123 | final Stat stat = new Stat(); 124 | byte[] subspacePrefix = nodeStatSubspace.pack(); 125 | 126 | for (KeyValue keyValue : keyValues) { 127 | if (!ByteArrayUtil.startsWith(keyValue.getKey(), subspacePrefix)) { 128 | continue; 129 | } 130 | 131 | StatKey statKey = StatKey.from(nodeStatSubspace.unpack(keyValue.getKey()).getBytes(0)); 132 | 133 | switch (statKey) { 134 | case CZXID: 135 | stat.setCzxid(Longs.fromByteArray(keyValue.getValue())); 136 | break; 137 | case MZXID: 138 | stat.setMzxid(Longs.fromByteArray(keyValue.getValue())); 139 | break; 140 | case PZXID: 141 | stat.setPzxid(Longs.fromByteArray(keyValue.getValue())); 142 | break; 143 | case CTIME: 144 | stat.setCtime(Longs.fromByteArray(keyValue.getValue())); 145 | break; 146 | case MTIME: 147 | stat.setMtime(Longs.fromByteArray(keyValue.getValue())); 148 | break; 149 | case VERSION: 150 | stat.setVersion(Ints.fromByteArray(keyValue.getValue())); 151 | break; 152 | case AVERSION: 153 | stat.setAversion(Ints.fromByteArray(keyValue.getValue())); 154 | break; 155 | case CVERSION: 156 | stat.setCversion(Ints.fromByteArray(keyValue.getValue())); 157 | break; 158 | case NUM_CHILDREN: 159 | stat.setNumChildren(Ints.fromByteArray(keyValue.getValue())); 160 | break; 161 | case EPHEMERAL_OWNER: 162 | long sessionId = Longs.fromByteArray(keyValue.getValue()); 163 | 164 | if (sessionId == -1) { 165 | break; 166 | } 167 | 168 | stat.setEphemeralOwner(sessionId); 169 | break; 170 | case DATA_LENGTH: 171 | stat.setDataLength(Ints.fromByteArray(keyValue.getValue())); 172 | break; 173 | } 174 | } 175 | 176 | return stat; 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/layer/FdbNodeWriter.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.layer; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Map.Entry; 7 | import java.util.Optional; 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | import org.apache.zookeeper.data.ACL; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import com.apple.foundationdb.KeyValue; 15 | import com.apple.foundationdb.MutationType; 16 | import com.apple.foundationdb.Range; 17 | import com.apple.foundationdb.Transaction; 18 | import com.apple.foundationdb.directory.DirectorySubspace; 19 | import com.apple.foundationdb.subspace.Subspace; 20 | import com.apple.foundationdb.tuple.ByteArrayUtil; 21 | import com.apple.foundationdb.tuple.Tuple; 22 | import com.apple.foundationdb.tuple.Versionstamp; 23 | import com.google.common.annotations.VisibleForTesting; 24 | import com.google.common.io.ByteArrayDataOutput; 25 | import com.google.common.io.ByteStreams; 26 | import com.google.common.primitives.Ints; 27 | import com.google.common.primitives.Longs; 28 | import com.google.inject.Inject; 29 | import com.ph14.fdb.zk.ByteUtil; 30 | import com.ph14.fdb.zk.FdbSchemaConstants; 31 | 32 | public class FdbNodeWriter { 33 | 34 | private static final Logger LOG = LoggerFactory.getLogger(FdbNodeWriter.class); 35 | 36 | public static final long VERSIONSTAMP_FLAG = Long.MIN_VALUE; 37 | 38 | private static final byte[] INITIAL_VERSION = Ints.toByteArray(0); 39 | 40 | // not in use yet... need to store all values as little-endian for mutations to work 41 | // unfortunately Java is big-endian by comparison which makes this a little messier 42 | public static final long INCREMENT_FLAG = Long.MIN_VALUE + 1; 43 | public static final long DECREMENT_FLAG = Long.MIN_VALUE + 2; 44 | private static final byte[] INCREMENT_LONG = Longs.toByteArray(1); 45 | private static final byte[] DECREMENT_LONG = ByteArrayUtil.encodeInt(-1); 46 | 47 | private final byte[] versionstampValue; 48 | 49 | @Inject 50 | public FdbNodeWriter() { 51 | this.versionstampValue = Tuple.from(Versionstamp.incomplete()).packWithVersionstamp(); 52 | } 53 | 54 | public void createNewNode(Transaction transaction, Subspace nodeSubspace, FdbNode fdbNode) { 55 | writeStat(transaction, nodeSubspace, fdbNode); 56 | writeData(transaction, nodeSubspace, fdbNode.getData()); 57 | writeACLs(transaction, nodeSubspace, fdbNode.getAcls()); 58 | } 59 | 60 | public void writeData(Transaction transaction, Subspace nodeSubspace, byte[] data) { 61 | int dataLength = data.length; 62 | 63 | if (dataLength > FdbSchemaConstants.ZK_MAX_DATA_LENGTH) { 64 | // this is the actual ZK error. it uses jute.maxBuffer + 1024 as the max znode size 65 | throw new RuntimeException(new IOException("Unreasonable length " + dataLength)); 66 | } 67 | 68 | transaction.clear(new Range( 69 | nodeSubspace.pack(Tuple.from(FdbSchemaConstants.DATA_KEY, 0)), 70 | nodeSubspace.pack(Tuple.from(FdbSchemaConstants.DATA_KEY, Integer.MAX_VALUE)) 71 | )); 72 | 73 | List dataBlocks = ByteUtil.divideByteArray(data, FdbSchemaConstants.FDB_MAX_VALUE_SIZE); 74 | 75 | for (int i = 0; i < dataBlocks.size(); i++) { 76 | transaction.set( 77 | nodeSubspace.pack(Tuple.from(FdbSchemaConstants.DATA_KEY, i)), 78 | dataBlocks.get(i) 79 | ); 80 | } 81 | } 82 | 83 | public void writeStat(Transaction transaction, Subspace nodeSubspace, Map newValues) { 84 | Subspace nodeStatSubspace = nodeSubspace.get(FdbSchemaConstants.STAT_KEY); 85 | 86 | for (Entry entry : newValues.entrySet()) { 87 | if (entry.getValue() == VERSIONSTAMP_FLAG) { 88 | transaction.mutate(MutationType.SET_VERSIONSTAMPED_VALUE, entry.getKey().toKey(nodeStatSubspace), versionstampValue); 89 | } else if (entry.getValue() == INCREMENT_FLAG) { 90 | transaction.mutate(MutationType.ADD, entry.getKey().toKey(nodeStatSubspace), INCREMENT_LONG); 91 | } else if (entry.getValue() == DECREMENT_FLAG) { 92 | transaction.mutate(MutationType.ADD, entry.getKey().toKey(nodeStatSubspace), DECREMENT_LONG); 93 | } else { 94 | KeyValue keyValue = entry.getKey().toKeyValue(nodeStatSubspace, entry.getValue()); 95 | transaction.set(keyValue.getKey(), keyValue.getValue()); 96 | } 97 | } 98 | } 99 | 100 | public CompletableFuture deleteNodeAsync(Transaction transaction, DirectorySubspace nodeSubspace) { 101 | return nodeSubspace.remove(transaction); 102 | } 103 | 104 | private void writeStat(Transaction transaction, Subspace nodeSubspace, FdbNode fdbNode) { 105 | if (fdbNode.getStat() == null) { 106 | writeStatForNewNode(transaction, nodeSubspace, fdbNode.getData(), fdbNode.getEphemeralSessionId()); 107 | } else { 108 | writeStatFromExistingNode(transaction, nodeSubspace, fdbNode); 109 | } 110 | } 111 | 112 | private void writeACLs(Transaction transaction, Subspace nodeSubspace, List acls) { 113 | for (int i = 0; i < acls.size(); i++) { 114 | try { 115 | ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput(); 116 | acls.get(i).write(dataOutput); 117 | 118 | transaction.set( 119 | nodeSubspace.pack(Tuple.from(FdbSchemaConstants.ACL_KEY, i)), 120 | dataOutput.toByteArray() 121 | ); 122 | } catch (IOException e) { 123 | LOG.error("Not ideal", e); 124 | } 125 | } 126 | } 127 | 128 | private void writeStatForNewNode(Transaction transaction, Subspace nodeSubspace, byte[] data, Optional ephemeralOwnerId) { 129 | byte[] now = Longs.toByteArray(System.currentTimeMillis()); 130 | Subspace nodeStatSubspace = nodeSubspace.get(FdbSchemaConstants.STAT_KEY); 131 | 132 | transaction.mutate(MutationType.SET_VERSIONSTAMPED_VALUE, StatKey.CZXID.toKey(nodeStatSubspace), versionstampValue); 133 | transaction.mutate(MutationType.SET_VERSIONSTAMPED_VALUE, StatKey.MZXID.toKey(nodeStatSubspace), versionstampValue); 134 | 135 | transaction.set(StatKey.CTIME.toKey(nodeStatSubspace), now); 136 | transaction.set(StatKey.MTIME.toKey(nodeStatSubspace), now); 137 | transaction.set(StatKey.DATA_LENGTH.toKey(nodeStatSubspace), StatKey.DATA_LENGTH.getValue(data.length)); 138 | transaction.set(StatKey.NUM_CHILDREN.toKey(nodeStatSubspace), Ints.toByteArray(0)); 139 | transaction.set(StatKey.VERSION.toKey(nodeStatSubspace), INITIAL_VERSION); 140 | transaction.set(StatKey.CVERSION.toKey(nodeStatSubspace), INITIAL_VERSION); 141 | transaction.set(StatKey.AVERSION.toKey(nodeStatSubspace), INITIAL_VERSION); 142 | transaction.set(StatKey.EPHEMERAL_OWNER.toKey(nodeStatSubspace), StatKey.EPHEMERAL_OWNER.getValue(ephemeralOwnerId.orElse(-1L))); 143 | } 144 | 145 | @VisibleForTesting 146 | void writeStatFromExistingNode(Transaction transaction, Subspace nodeSubspace, FdbNode fdbNode) { 147 | Subspace nodeStatSubspace = nodeSubspace.get(FdbSchemaConstants.STAT_KEY); 148 | 149 | transaction.set( 150 | StatKey.CZXID.toKey(nodeStatSubspace), 151 | StatKey.CZXID.getValue(fdbNode.getStat().getCzxid())); 152 | transaction.set( 153 | StatKey.MZXID.toKey(nodeStatSubspace), 154 | StatKey.MZXID.getValue(fdbNode.getStat().getMzxid())); 155 | transaction.set( 156 | StatKey.CTIME.toKey(nodeStatSubspace), 157 | StatKey.CTIME.getValue(fdbNode.getStat().getCtime())); 158 | transaction.set( 159 | StatKey.MTIME.toKey(nodeStatSubspace), 160 | StatKey.MTIME.getValue(fdbNode.getStat().getMtime())); 161 | transaction.set( 162 | StatKey.DATA_LENGTH.toKey(nodeStatSubspace), 163 | StatKey.DATA_LENGTH.getValue(fdbNode.getStat().getDataLength())); 164 | transaction.set( 165 | StatKey.NUM_CHILDREN.toKey(nodeStatSubspace), 166 | StatKey.NUM_CHILDREN.getValue(fdbNode.getStat().getNumChildren())); 167 | transaction.set( 168 | StatKey.VERSION.toKey(nodeStatSubspace), 169 | StatKey.VERSION.getValue(fdbNode.getStat().getVersion())); 170 | transaction.set( 171 | StatKey.CVERSION.toKey(nodeStatSubspace), 172 | StatKey.CVERSION.getValue(fdbNode.getStat().getCversion())); 173 | transaction.set( 174 | StatKey.AVERSION.toKey(nodeStatSubspace), 175 | StatKey.AVERSION.getValue(fdbNode.getStat().getAversion())); 176 | transaction.set( 177 | StatKey.EPHEMERAL_OWNER.toKey(nodeStatSubspace), 178 | StatKey.EPHEMERAL_OWNER.getValue(fdbNode.getStat().getEphemeralOwner())); 179 | } 180 | 181 | 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/layer/FdbPath.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.layer; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import org.apache.zookeeper.common.PathUtils; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import com.google.common.base.Joiner; 12 | import com.google.common.base.Splitter; 13 | import com.google.common.collect.ImmutableList; 14 | 15 | public class FdbPath { 16 | 17 | private static final Logger LOG = LoggerFactory.getLogger(FdbPath.class); 18 | 19 | public static final String ROOT_PATH = "fdb-zk-root"; 20 | 21 | private static final Splitter SPLITTER = Splitter.on("/"); 22 | private static final Joiner JOINER = Joiner.on("/"); 23 | 24 | public static List toFdbPath(String zkPath) { 25 | PathUtils.validatePath(zkPath); 26 | 27 | List path = ImmutableList.copyOf(SPLITTER.split(zkPath)) 28 | .stream() 29 | .filter(s -> !s.isEmpty()) 30 | .collect(Collectors.toList()); 31 | 32 | if (path.size() == 0) { 33 | return Collections.singletonList(ROOT_PATH); 34 | } else { 35 | return ImmutableList.builder() 36 | .add(ROOT_PATH) 37 | .addAll(path) 38 | .build(); 39 | } 40 | } 41 | 42 | public static String toZkPath(List fdbPath) { 43 | return "/" + JOINER.join(fdbPath.subList(1, fdbPath.size())); 44 | } 45 | 46 | public static List toFdbParentPath(String zkPath) { 47 | List fullFdbPath = toFdbPath(zkPath); 48 | return fullFdbPath.subList(0, fullFdbPath.size() - 1); 49 | } 50 | 51 | public static String toZkParentPath(String zkPath) { 52 | int lastSlash = zkPath.lastIndexOf('/'); 53 | 54 | if (lastSlash == -1 || zkPath.indexOf('\0') != -1) { 55 | // throw new KeeperException.BadArgumentsException(zkPath); 56 | } 57 | 58 | // how to handle this properly? how does ZK fix this? 59 | if (lastSlash == 0) { 60 | return "/"; 61 | } 62 | 63 | return zkPath.substring(0, lastSlash); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/layer/FdbWatchManager.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.layer; 2 | 3 | import org.apache.zookeeper.Watcher; 4 | import org.apache.zookeeper.Watcher.Event.EventType; 5 | 6 | import com.apple.foundationdb.Transaction; 7 | import com.google.inject.Inject; 8 | import com.ph14.fdb.zk.layer.changefeed.WatchEventChangefeed; 9 | 10 | // TODO: Still need this class? 11 | public class FdbWatchManager { 12 | 13 | private final WatchEventChangefeed watchChangefeed; 14 | 15 | @Inject 16 | public FdbWatchManager(WatchEventChangefeed watchChangefeed) { 17 | this.watchChangefeed = watchChangefeed; 18 | } 19 | 20 | public void checkForWatches(long sessionId, Watcher watcher) { 21 | watchChangefeed.playChangefeed(sessionId, watcher); 22 | } 23 | 24 | public void triggerNodeCreatedWatch(Transaction transaction, String zkPath) { 25 | watchChangefeed.appendToChangefeed(transaction, EventType.NodeCreated, zkPath).join(); 26 | } 27 | 28 | public void triggerNodeUpdatedWatch(Transaction transaction, String zkPath) { 29 | watchChangefeed.appendToChangefeed(transaction, EventType.NodeDataChanged, zkPath).join(); 30 | } 31 | 32 | public void triggerNodeDeletedWatch(Transaction transaction, String zkPath) { 33 | watchChangefeed.appendToChangefeed(transaction, EventType.NodeDeleted, zkPath).join(); 34 | } 35 | 36 | public void triggerNodeChildrenWatch(Transaction transaction, String zkPath) { 37 | watchChangefeed.appendToChangefeed(transaction, EventType.NodeChildrenChanged, zkPath).join(); 38 | } 39 | 40 | public void addNodeCreatedWatch(Transaction transaction, String zkPath, Watcher watcher, long sessionId) { 41 | watchChangefeed.setZKChangefeedWatch(transaction, watcher, sessionId, EventType.NodeCreated, zkPath); 42 | } 43 | 44 | public void addNodeDataUpdatedWatch(Transaction transaction, String zkPath, Watcher watcher, long sessionId) { 45 | watchChangefeed.setZKChangefeedWatch(transaction, watcher, sessionId, EventType.NodeDataChanged, zkPath); 46 | } 47 | 48 | public void addNodeDeletedWatch(Transaction transaction, String zkPath, Watcher watcher, long sessionId) { 49 | watchChangefeed.setZKChangefeedWatch(transaction, watcher, sessionId, EventType.NodeDeleted, zkPath); 50 | } 51 | 52 | public void addNodeChildrenWatch(Transaction transaction, String zkPath, Watcher watcher, long sessionId) { 53 | watchChangefeed.setZKChangefeedWatch(transaction, watcher, sessionId, EventType.NodeChildrenChanged, zkPath); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/layer/StatKey.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.layer; 2 | 3 | import java.util.Arrays; 4 | import java.util.Map; 5 | import java.util.function.Function; 6 | 7 | import com.apple.foundationdb.KeyValue; 8 | import com.apple.foundationdb.subspace.Subspace; 9 | import com.google.common.collect.ImmutableMap; 10 | import com.google.common.primitives.Ints; 11 | import com.google.common.primitives.Longs; 12 | import com.google.common.primitives.Shorts; 13 | 14 | public enum StatKey { 15 | // Creation ZXID. Use Versionstamp at creation time 16 | CZXID(1, StatKeyValueType.LONG), 17 | // Modification ZXID. Use Versionstamp at creation time / data modification time 18 | MZXID(2, StatKeyValueType.LONG), 19 | // Parent ZXID. Use versionstamp at creation / child creation+deletion time 20 | PZXID(3, StatKeyValueType.LONG), 21 | // Creation timestamp, epoch millis 22 | CTIME(4, StatKeyValueType.LONG), 23 | // Modification timestamp, epoch millis 24 | MTIME(5, StatKeyValueType.LONG), 25 | // # of updates to this node 26 | VERSION(6, StatKeyValueType.INT), 27 | // # of updates to this node's children 28 | CVERSION(7, StatKeyValueType.INT), 29 | // # of updates to this node's ACL policies 30 | AVERSION(8, StatKeyValueType.INT), 31 | // Session ID of ephemeral owner. -1 if permanent 32 | EPHEMERAL_OWNER(9, StatKeyValueType.LONG), 33 | // Num bytes of data in node 34 | DATA_LENGTH(10, StatKeyValueType.INT), 35 | // Number of children of this node 36 | NUM_CHILDREN(11, StatKeyValueType.INT) 37 | ; 38 | 39 | private enum StatKeyValueType { 40 | INT, 41 | LONG 42 | } 43 | 44 | private static final Map INDEX = Arrays.stream(StatKey.values()) 45 | .collect(ImmutableMap.toImmutableMap( 46 | k -> k.statKeyId, 47 | Function.identity() 48 | )); 49 | 50 | 51 | private final short statKeyId; 52 | private final StatKeyValueType statKeyValueType; 53 | 54 | StatKey(int statKeyId, StatKeyValueType statKeyValueType) { 55 | this.statKeyId = (short) statKeyId; 56 | this.statKeyValueType = statKeyValueType; 57 | } 58 | 59 | public byte[] getStatKeyId() { 60 | return Shorts.toByteArray(statKeyId); 61 | } 62 | 63 | public byte[] getValue(long value) { 64 | switch (statKeyValueType) { 65 | case INT: 66 | return Ints.toByteArray((int) value); 67 | case LONG: 68 | return Longs.toByteArray(value); 69 | default: 70 | throw new RuntimeException("unknown stat key value type: " + statKeyValueType); 71 | } 72 | } 73 | 74 | public byte[] toKey(Subspace nodeStatSubspace) { 75 | return nodeStatSubspace.get(getStatKeyId()).pack(); 76 | } 77 | 78 | public KeyValue toKeyValue(Subspace nodeStatSubspace, byte[] value) { 79 | return new KeyValue(toKey(nodeStatSubspace), value); 80 | } 81 | 82 | public KeyValue toKeyValue(Subspace nodeStatSubspace, long value) { 83 | return new KeyValue(toKey(nodeStatSubspace), getValue(value)); 84 | } 85 | 86 | public static StatKey from(byte[] statKeyId) { 87 | return INDEX.get(Shorts.fromByteArray(statKeyId)); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/layer/changefeed/ChangefeedWatchEvent.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.layer.changefeed; 2 | 3 | import java.util.Objects; 4 | 5 | import org.apache.zookeeper.Watcher.Event.EventType; 6 | 7 | import com.apple.foundationdb.tuple.Versionstamp; 8 | import com.google.common.base.MoreObjects; 9 | 10 | public class ChangefeedWatchEvent { 11 | 12 | private final Versionstamp versionstamp; 13 | private final EventType eventType; 14 | private final String zkPath; 15 | 16 | public ChangefeedWatchEvent(Versionstamp versionstamp, EventType eventType, String zkPath) { 17 | this.versionstamp = versionstamp; 18 | this.eventType = eventType; 19 | this.zkPath = zkPath; 20 | } 21 | 22 | public Versionstamp getVersionstamp() { 23 | return versionstamp; 24 | } 25 | 26 | public EventType getEventType() { 27 | return eventType; 28 | } 29 | 30 | public String getZkPath() { 31 | return zkPath; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object obj) { 36 | if (this == obj) { 37 | return true; 38 | } 39 | if (obj instanceof ChangefeedWatchEvent) { 40 | final ChangefeedWatchEvent that = (ChangefeedWatchEvent) obj; 41 | return Objects.equals(this.getVersionstamp(), that.getVersionstamp()) 42 | && Objects.equals(this.getEventType(), that.getEventType()) 43 | && Objects.equals(this.getZkPath(), that.getZkPath()); 44 | } 45 | return false; 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | return Objects.hash(getVersionstamp(), getEventType(), getZkPath()); 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return MoreObjects.toStringHelper(this) 56 | .add("versionstamp", versionstamp) 57 | .add("eventType", eventType) 58 | .add("zkPath", zkPath) 59 | .toString(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/layer/changefeed/WatchEventChangefeed.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.layer.changefeed; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.locks.ReentrantLock; 10 | 11 | import org.apache.zookeeper.WatchedEvent; 12 | import org.apache.zookeeper.Watcher; 13 | import org.apache.zookeeper.Watcher.Event.EventType; 14 | import org.apache.zookeeper.Watcher.Event.KeeperState; 15 | 16 | import com.apple.foundationdb.Database; 17 | import com.apple.foundationdb.KeyValue; 18 | import com.apple.foundationdb.MutationType; 19 | import com.apple.foundationdb.Range; 20 | import com.apple.foundationdb.Transaction; 21 | import com.apple.foundationdb.tuple.ByteArrayUtil; 22 | import com.apple.foundationdb.tuple.Tuple; 23 | import com.apple.foundationdb.tuple.Versionstamp; 24 | import com.google.common.collect.ImmutableList; 25 | import com.google.common.collect.Iterables; 26 | import com.google.inject.Inject; 27 | import com.google.inject.Singleton; 28 | 29 | @Singleton 30 | public class WatchEventChangefeed { 31 | 32 | private static final String ACTIVE_WATCH_NAMESPACE = "fdb-zk-watch-active"; 33 | private static final String CHANGEFEED_TRIGGER_NAMESPACE = "fdb-zk-watch-trigger"; 34 | private static final String CHANGEFEED_NAMESPACE = "fdb-zk-watch-cf"; 35 | 36 | private static final byte[] EMPTY_VALUE = new byte[0]; 37 | 38 | // TODO: this can currently grow without bound, since it doesn't evict stale Watchers 39 | private final ConcurrentHashMap changefeedLocks; 40 | 41 | private final Database database; 42 | 43 | @Inject 44 | public WatchEventChangefeed(Database database) { 45 | this.database = database; 46 | this.changefeedLocks = new ConcurrentHashMap<>(); 47 | } 48 | 49 | /** 50 | * Sets a ZK Watch on a particular ZKPath for a given event type. 51 | * 52 | * To do this, it: 53 | * 54 | * 1. Marks that the client is actively watching a given (zkPath, eventType) 55 | * 2. Subscribes the client to a FDB watch for the ZK Watch Trigger 56 | */ 57 | public CompletableFuture setZKChangefeedWatch(Transaction transaction, Watcher watcher, long sessionId, EventType eventType, String zkPath) { 58 | // register as a ZK watcher for a given path + event type 59 | transaction.set(getActiveWatchKey(sessionId, zkPath, eventType), EMPTY_VALUE); 60 | 61 | // subscribe to the trigger-key, to avoid having to poll for watch events 62 | CompletableFuture triggerFuture = transaction.watch(getTriggerKey(sessionId, eventType)); 63 | return triggerFuture.whenComplete((v, e) -> playChangefeed(sessionId, watcher)); 64 | } 65 | 66 | /** 67 | * * Add an update to the active-watcher changefeeds for a (session, watchEventType, zkPath) 68 | * * Triggers all FDB watches for the changefeed 69 | */ 70 | public CompletableFuture appendToChangefeed(Transaction transaction, EventType eventType, String zkPath) { 71 | Range activeWatches = Range.startsWith(Tuple.from(ACTIVE_WATCH_NAMESPACE, zkPath, eventType.getIntValue()).pack()); 72 | 73 | return transaction.getRange(activeWatches).asList() 74 | .thenApply(keyValues -> { 75 | for (KeyValue keyValue : keyValues) { 76 | long sessionId = Tuple.fromBytes(keyValue.getKey()).getLong(3); 77 | 78 | Tuple changefeedKey = Tuple.from(CHANGEFEED_NAMESPACE, sessionId, Versionstamp.incomplete(), eventType.getIntValue()); 79 | // append the event to the changefeed of each ZK watcher 80 | transaction.mutate(MutationType.SET_VERSIONSTAMPED_KEY, changefeedKey.packWithVersionstamp(), Tuple.from(zkPath).pack()); 81 | 82 | // update the watched trigger-key for each ZK watcher, so they know to check their changefeeds for updates 83 | transaction.mutate( 84 | MutationType.SET_VERSIONSTAMPED_VALUE, 85 | Tuple.from(CHANGEFEED_TRIGGER_NAMESPACE, sessionId, eventType.getIntValue()).pack(), 86 | Tuple.from(Versionstamp.incomplete()).packWithVersionstamp()); 87 | } 88 | 89 | transaction.clear(activeWatches); 90 | return null; 91 | }); 92 | } 93 | 94 | public void playChangefeed(long sessionId, Watcher watcher) { 95 | synchronized (changefeedLocks) { 96 | changefeedLocks.computeIfAbsent(watcher, x -> new ReentrantLock()); 97 | } 98 | 99 | try { 100 | // We don't want concurrent changefeed lookups to duplicate events to the same client. 101 | changefeedLocks.get(watcher).lock(); 102 | 103 | Range allAvailableZKWatchEvents = Range.startsWith(Tuple.from(CHANGEFEED_NAMESPACE, sessionId).pack()); 104 | 105 | List watchEvents = database.run( 106 | transaction -> transaction.getRange(allAvailableZKWatchEvents).asList()) 107 | .thenApply(kvs -> kvs.stream() 108 | .map(WatchEventChangefeed::toWatchEvent) 109 | .collect(ImmutableList.toImmutableList())) 110 | .join(); 111 | 112 | if (watchEvents.isEmpty()) { 113 | return; 114 | } 115 | 116 | Set seenCommitVersionstamps = new HashSet<>(watchEvents.size()); 117 | for (ChangefeedWatchEvent watchEvent : watchEvents) { 118 | ByteBuffer watchCommitVersion = ByteBuffer.wrap(watchEvent.getVersionstamp().getTransactionVersion()); 119 | 120 | // in the case that one transaction committed multiple watches for different event types (e.g. getData), 121 | // we only want to trigger one of the watches. TBH this might only be needed for one of the test cases, not sure 122 | if (seenCommitVersionstamps.add(watchCommitVersion)) { 123 | watcher.process(new WatchedEvent(watchEvent.getEventType(), KeeperState.SyncConnected, watchEvent.getZkPath())); 124 | } 125 | } 126 | 127 | Versionstamp greatestVersionstamp = Iterables.getLast(watchEvents).getVersionstamp(); 128 | database.run(transaction -> { 129 | // if watch entries were written between reading and executing, we want to only remove entries up to what we just had 130 | byte[] lastProcessedEvent = ByteArrayUtil.strinc(Tuple.from(CHANGEFEED_NAMESPACE, sessionId, greatestVersionstamp).pack()); 131 | transaction.clear(allAvailableZKWatchEvents.begin, lastProcessedEvent); 132 | 133 | return null; 134 | }); 135 | } finally { 136 | changefeedLocks.get(watcher).unlock(); 137 | } 138 | } 139 | 140 | public CompletableFuture clearAllWatchesForSession(Transaction transaction, long sessionId) { 141 | Range allAvailableZKWatchEvents = Range.startsWith(Tuple.from(CHANGEFEED_NAMESPACE, sessionId).pack()); 142 | 143 | return transaction.getRange(allAvailableZKWatchEvents).asList() 144 | .thenApply(kvs -> { 145 | kvs.stream() 146 | .map(WatchEventChangefeed::toWatchEvent) 147 | .forEach(watchEvent -> { 148 | transaction.clear(getActiveWatchKey(sessionId, watchEvent.getZkPath(), watchEvent.getEventType())); 149 | transaction.clear(getTriggerKey(sessionId, watchEvent.getEventType())); 150 | }); 151 | 152 | transaction.clear(allAvailableZKWatchEvents); 153 | return null; 154 | }); 155 | } 156 | 157 | private static byte[] getActiveWatchKey(long sessionId, String zkPath, EventType eventType) { 158 | return Tuple.from(ACTIVE_WATCH_NAMESPACE, zkPath, eventType.getIntValue(), sessionId).pack(); 159 | } 160 | 161 | private static byte[] getTriggerKey(long sessionId, EventType eventType) { 162 | return Tuple.from(CHANGEFEED_TRIGGER_NAMESPACE, sessionId, eventType.getIntValue()).pack(); 163 | } 164 | 165 | private static ChangefeedWatchEvent toWatchEvent(KeyValue keyValue) { 166 | Tuple tuple = Tuple.fromBytes(keyValue.getKey()); 167 | Versionstamp versionstamp = tuple.getVersionstamp(2); 168 | EventType eventType = EventType.fromInt((int) tuple.getLong(3)); // tuple layer upcasts ints to longs 169 | String zkPath = Tuple.fromBytes(keyValue.getValue()).getString(0); 170 | 171 | return new ChangefeedWatchEvent(versionstamp, eventType, zkPath); 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/layer/ephemeral/FdbEphemeralNodeManager.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.layer.ephemeral; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | import com.apple.foundationdb.Range; 6 | import com.apple.foundationdb.Transaction; 7 | import com.apple.foundationdb.subspace.Subspace; 8 | import com.apple.foundationdb.tuple.Tuple; 9 | import com.google.common.base.Charsets; 10 | import com.google.common.collect.Iterables; 11 | import com.google.inject.Inject; 12 | import com.google.inject.Singleton; 13 | 14 | @Singleton 15 | public class FdbEphemeralNodeManager { 16 | 17 | private static final String EPHEMERAL_NODE_SUBSPACE = "fdb-zk-ephemeral-nodes"; 18 | private static final byte[] EMPTY_VALUE = new byte[0]; 19 | 20 | private final byte[] ephemeralNodeSubspace; 21 | 22 | @Inject 23 | public FdbEphemeralNodeManager() { 24 | this.ephemeralNodeSubspace = new Subspace(EPHEMERAL_NODE_SUBSPACE.getBytes(Charsets.UTF_8)).pack(); 25 | } 26 | 27 | public void addEphemeralNode(Transaction transaction, String zkPath, long sessionId) { 28 | transaction.set(Tuple.from(ephemeralNodeSubspace, sessionId, zkPath).pack(), EMPTY_VALUE); 29 | } 30 | 31 | public CompletableFuture> getEphemeralNodeZkPaths(Transaction transaction, long sessionId) { 32 | return transaction.getRange(Range.startsWith(Tuple.from(ephemeralNodeSubspace, sessionId).pack())) 33 | .asList() 34 | .thenApply(kvs -> Iterables.transform( 35 | kvs, 36 | kv -> Tuple.fromBytes(kv.getKey()).getString(2))); 37 | } 38 | 39 | public void removeNode(Transaction transaction, String zkPath, long sessionId) { 40 | transaction.clear(Tuple.from(ephemeralNodeSubspace, sessionId, zkPath).pack()); 41 | } 42 | 43 | public void clearEphemeralNodesForSession(Transaction transaction, long sessionId) { 44 | transaction.clear(Range.startsWith(Tuple.from(ephemeralNodeSubspace, sessionId).pack())); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ops/FdbCheckVersionOp.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CompletableFuture; 5 | import java.util.concurrent.CompletionException; 6 | 7 | import org.apache.zookeeper.KeeperException; 8 | import org.apache.zookeeper.KeeperException.APIErrorException; 9 | import org.apache.zookeeper.KeeperException.BadVersionException; 10 | import org.apache.zookeeper.KeeperException.NoNodeException; 11 | import org.apache.zookeeper.data.Stat; 12 | import org.apache.zookeeper.proto.CheckVersionRequest; 13 | import org.apache.zookeeper.server.Request; 14 | 15 | import com.apple.foundationdb.Transaction; 16 | import com.apple.foundationdb.directory.DirectoryLayer; 17 | import com.apple.foundationdb.directory.DirectorySubspace; 18 | import com.apple.foundationdb.directory.NoSuchDirectoryException; 19 | import com.google.inject.Inject; 20 | import com.hubspot.algebra.Result; 21 | import com.hubspot.algebra.Results; 22 | import com.ph14.fdb.zk.layer.FdbNodeReader; 23 | import com.ph14.fdb.zk.layer.FdbPath; 24 | 25 | public class FdbCheckVersionOp implements FdbOp { 26 | 27 | private final FdbNodeReader fdbNodeReader; 28 | 29 | @Inject 30 | public FdbCheckVersionOp(FdbNodeReader fdbNodeReader) { 31 | this.fdbNodeReader = fdbNodeReader; 32 | } 33 | 34 | @Override 35 | public CompletableFuture> execute(Request zkRequest, Transaction transaction, CheckVersionRequest request) { 36 | List path = FdbPath.toFdbPath(request.getPath()); 37 | 38 | final DirectorySubspace subspace; 39 | final Stat stat; 40 | try { 41 | subspace = DirectoryLayer.getDefault().open(transaction, path).join(); 42 | stat = fdbNodeReader.getNodeStat(subspace, transaction).join(); 43 | 44 | if (request.getVersion() != -1 && stat.getVersion() != request.getVersion()) { 45 | return CompletableFuture.completedFuture(Result.err(new BadVersionException(request.getPath()))); 46 | } 47 | } catch (CompletionException e) { 48 | if (e.getCause() instanceof NoSuchDirectoryException) { 49 | return CompletableFuture.completedFuture(Results.err(new NoNodeException(request.getPath()))); 50 | } else { 51 | return CompletableFuture.completedFuture(Results.err(new APIErrorException())); 52 | } 53 | } 54 | 55 | return CompletableFuture.completedFuture(Result.ok(null)); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ops/FdbCreateOp.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.Locale; 4 | import java.util.Optional; 5 | import java.util.concurrent.CompletableFuture; 6 | import java.util.concurrent.CompletionException; 7 | 8 | import org.apache.zookeeper.CreateMode; 9 | import org.apache.zookeeper.KeeperException; 10 | import org.apache.zookeeper.KeeperException.NoChildrenForEphemeralsException; 11 | import org.apache.zookeeper.KeeperException.NodeExistsException; 12 | import org.apache.zookeeper.data.Stat; 13 | import org.apache.zookeeper.proto.CreateRequest; 14 | import org.apache.zookeeper.proto.CreateResponse; 15 | import org.apache.zookeeper.server.Request; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import com.apple.foundationdb.Transaction; 20 | import com.apple.foundationdb.directory.DirectoryAlreadyExistsException; 21 | import com.apple.foundationdb.directory.DirectoryLayer; 22 | import com.apple.foundationdb.directory.DirectorySubspace; 23 | import com.apple.foundationdb.directory.NoSuchDirectoryException; 24 | import com.google.common.collect.ImmutableMap; 25 | import com.google.inject.Inject; 26 | import com.hubspot.algebra.Result; 27 | import com.ph14.fdb.zk.layer.FdbNode; 28 | import com.ph14.fdb.zk.layer.FdbNodeReader; 29 | import com.ph14.fdb.zk.layer.FdbNodeWriter; 30 | import com.ph14.fdb.zk.layer.FdbPath; 31 | import com.ph14.fdb.zk.layer.FdbWatchManager; 32 | import com.ph14.fdb.zk.layer.StatKey; 33 | import com.ph14.fdb.zk.layer.ephemeral.FdbEphemeralNodeManager; 34 | 35 | public class FdbCreateOp implements FdbOp { 36 | 37 | private static final Logger LOG = LoggerFactory.getLogger(FdbCreateOp.class); 38 | 39 | private final FdbNodeReader fdbNodeReader; 40 | private final FdbNodeWriter fdbNodeWriter; 41 | private final FdbWatchManager fdbWatchManager; 42 | private final FdbEphemeralNodeManager fdbEphemeralNodeManager; 43 | 44 | @Inject 45 | public FdbCreateOp(FdbNodeReader fdbNodeReader, 46 | FdbNodeWriter fdbNodeWriter, 47 | FdbWatchManager fdbWatchManager, 48 | FdbEphemeralNodeManager fdbEphemeralNodeManager) { 49 | this.fdbNodeReader = fdbNodeReader; 50 | this.fdbNodeWriter = fdbNodeWriter; 51 | this.fdbWatchManager = fdbWatchManager; 52 | this.fdbEphemeralNodeManager = fdbEphemeralNodeManager; 53 | } 54 | 55 | @Override 56 | public CompletableFuture> execute(Request zkRequest, Transaction transaction, CreateRequest request) { 57 | final CreateMode createMode; 58 | try { 59 | createMode = CreateMode.fromFlag(request.getFlags()); 60 | } catch (KeeperException e) { 61 | return CompletableFuture.completedFuture(Result.err(e)); 62 | } 63 | 64 | final DirectorySubspace parentSubspace; 65 | final Stat parentStat; 66 | 67 | try { 68 | parentSubspace = DirectoryLayer.getDefault().open(transaction, FdbPath.toFdbParentPath(request.getPath())).join(); 69 | parentStat = fdbNodeReader.getNodeStat(parentSubspace, transaction).join(); 70 | } catch (CompletionException e) { 71 | if (e.getCause() instanceof NoSuchDirectoryException) { 72 | LOG.error("Couldn't find parent: {} for {}", FdbPath.toFdbParentPath(request.getPath()), request.getPath()); 73 | return CompletableFuture.completedFuture(Result.err(new KeeperException.NoNodeException("parent: " + request.getPath()))); 74 | } else { 75 | LOG.error("Error completing request : {}. {}", request, e); 76 | return CompletableFuture.completedFuture(Result.err(new KeeperException.APIErrorException())); 77 | } 78 | } 79 | 80 | if (parentStat.getEphemeralOwner() != 0) { 81 | return CompletableFuture.completedFuture(Result.err(new NoChildrenForEphemeralsException())); 82 | } 83 | 84 | final String finalZkPath; 85 | final FdbNode fdbNode; 86 | final DirectorySubspace subspace; 87 | 88 | try { 89 | if (createMode.isSequential()) { 90 | finalZkPath = request.getPath() + String.format(Locale.ENGLISH, "%010d", parentStat.getCversion()); 91 | } else { 92 | finalZkPath = request.getPath(); 93 | } 94 | 95 | Optional ephemeralOwner = createMode.isEphemeral() ? Optional.of(zkRequest.sessionId) : Optional.empty(); 96 | 97 | fdbNode = new FdbNode(finalZkPath, null, request.getData(), request.getAcl(), ephemeralOwner); 98 | subspace = DirectoryLayer.getDefault().create(transaction, FdbPath.toFdbPath(finalZkPath)).join(); 99 | } catch (CompletionException e) { 100 | if (e.getCause() instanceof DirectoryAlreadyExistsException) { 101 | return CompletableFuture.completedFuture(Result.err(new NodeExistsException(request.getPath()))); 102 | } else { 103 | LOG.error("Error completing request : {}. {}", request, e); 104 | return CompletableFuture.completedFuture(Result.err(new KeeperException.SystemErrorException())); 105 | } 106 | } 107 | 108 | fdbNodeWriter.createNewNode(transaction, subspace, fdbNode); 109 | 110 | if (createMode.isEphemeral()) { 111 | fdbEphemeralNodeManager.addEphemeralNode(transaction, finalZkPath, zkRequest.sessionId); 112 | } 113 | 114 | // need atomic ops / little-endian storage if we want multis to work 115 | fdbNodeWriter.writeStat( 116 | transaction, 117 | parentSubspace, 118 | ImmutableMap.of( 119 | StatKey.PZXID, FdbNodeWriter.VERSIONSTAMP_FLAG, 120 | StatKey.CVERSION, parentStat.getCversion() + 1L, 121 | StatKey.NUM_CHILDREN, parentStat.getNumChildren() + 1L)); 122 | 123 | fdbWatchManager.triggerNodeCreatedWatch(transaction, request.getPath()); 124 | fdbWatchManager.triggerNodeChildrenWatch(transaction, FdbPath.toZkParentPath(request.getPath())); 125 | 126 | return CompletableFuture.completedFuture(Result.ok(new CreateResponse(finalZkPath))); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ops/FdbDeleteOp.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CompletableFuture; 5 | import java.util.concurrent.CompletionException; 6 | 7 | import org.apache.zookeeper.KeeperException; 8 | import org.apache.zookeeper.KeeperException.BadVersionException; 9 | import org.apache.zookeeper.KeeperException.NoNodeException; 10 | import org.apache.zookeeper.KeeperException.NotEmptyException; 11 | import org.apache.zookeeper.KeeperException.SystemErrorException; 12 | import org.apache.zookeeper.OpResult.DeleteResult; 13 | import org.apache.zookeeper.data.Stat; 14 | import org.apache.zookeeper.proto.DeleteRequest; 15 | import org.apache.zookeeper.server.Request; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import com.apple.foundationdb.Transaction; 20 | import com.apple.foundationdb.directory.DirectoryLayer; 21 | import com.apple.foundationdb.directory.DirectorySubspace; 22 | import com.apple.foundationdb.directory.NoSuchDirectoryException; 23 | import com.google.common.collect.ImmutableMap; 24 | import com.google.inject.Inject; 25 | import com.hubspot.algebra.Result; 26 | import com.hubspot.algebra.Results; 27 | import com.ph14.fdb.zk.layer.FdbNodeReader; 28 | import com.ph14.fdb.zk.layer.FdbNodeWriter; 29 | import com.ph14.fdb.zk.layer.FdbPath; 30 | import com.ph14.fdb.zk.layer.FdbWatchManager; 31 | import com.ph14.fdb.zk.layer.StatKey; 32 | import com.ph14.fdb.zk.layer.ephemeral.FdbEphemeralNodeManager; 33 | 34 | public class FdbDeleteOp implements FdbOp { 35 | 36 | private static final Logger LOG = LoggerFactory.getLogger(FdbDeleteOp.class); 37 | 38 | private static final int ALL_VERSIONS_FLAG = -1; 39 | 40 | private final FdbNodeReader fdbNodeReader; 41 | private final FdbNodeWriter fdbNodeWriter; 42 | private final FdbWatchManager fdbWatchManager; 43 | private final FdbEphemeralNodeManager fdbEphemeralNodeManager; 44 | 45 | @Inject 46 | public FdbDeleteOp(FdbNodeReader fdbNodeReader, 47 | FdbNodeWriter fdbNodeWriter, 48 | FdbWatchManager fdbWatchManager, 49 | FdbEphemeralNodeManager fdbEphemeralNodeManager) { 50 | this.fdbNodeReader = fdbNodeReader; 51 | this.fdbNodeWriter = fdbNodeWriter; 52 | this.fdbWatchManager = fdbWatchManager; 53 | this.fdbEphemeralNodeManager = fdbEphemeralNodeManager; 54 | } 55 | 56 | @Override 57 | public CompletableFuture> execute(Request zkRequest, Transaction transaction, DeleteRequest request) { 58 | List path = FdbPath.toFdbPath(request.getPath()); 59 | 60 | final DirectorySubspace nodeSubspace; 61 | final Stat stat; 62 | 63 | final DirectorySubspace parentNodeSubspace; 64 | final Stat parentStat; 65 | try { 66 | nodeSubspace = DirectoryLayer.getDefault().open(transaction, path).join(); 67 | stat = fdbNodeReader.getNodeStat(nodeSubspace, transaction).join(); 68 | 69 | if (stat.getNumChildren() != 0) { 70 | return CompletableFuture.completedFuture(Result.err(new NotEmptyException(request.getPath()))); 71 | } 72 | 73 | if (request.getVersion() != ALL_VERSIONS_FLAG && stat.getVersion() != request.getVersion()) { 74 | return CompletableFuture.completedFuture(Result.err(new BadVersionException(request.getPath()))); 75 | } 76 | } catch (CompletionException e) { 77 | if (e.getCause() instanceof NoSuchDirectoryException) { 78 | return CompletableFuture.completedFuture(Results.err(new NoNodeException(request.getPath()))); 79 | } else { 80 | LOG.error("Error completing request: {}, {}", zkRequest, e); 81 | return CompletableFuture.completedFuture(Results.err(new SystemErrorException())); 82 | } 83 | } 84 | 85 | parentNodeSubspace = DirectoryLayer.getDefault().open(transaction, FdbPath.toFdbParentPath(request.getPath())).join(); 86 | 87 | // could eliminate this read by using atomic ops and using endianness correctly 88 | parentStat = fdbNodeReader.getNodeStat(parentNodeSubspace, transaction, StatKey.CVERSION, StatKey.NUM_CHILDREN, StatKey.EPHEMERAL_OWNER).join(); 89 | 90 | fdbNodeWriter.writeStat(transaction, parentNodeSubspace, 91 | ImmutableMap.of( 92 | StatKey.PZXID, FdbNodeWriter.VERSIONSTAMP_FLAG, 93 | StatKey.CVERSION, parentStat.getCversion() + 1L, 94 | StatKey.NUM_CHILDREN, parentStat.getNumChildren() - 1L 95 | )); 96 | 97 | if (stat.getEphemeralOwner() != 0) { 98 | fdbEphemeralNodeManager.removeNode(transaction, request.getPath(), zkRequest.sessionId); 99 | } 100 | 101 | fdbNodeWriter.deleteNodeAsync(transaction, nodeSubspace).join(); 102 | 103 | fdbWatchManager.triggerNodeDeletedWatch(transaction, request.getPath()); 104 | fdbWatchManager.triggerNodeChildrenWatch(transaction, FdbPath.toZkParentPath(request.getPath())); 105 | 106 | return CompletableFuture.completedFuture(Result.ok(new DeleteResult())); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ops/FdbExistsOp.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CompletableFuture; 5 | import java.util.concurrent.CompletionException; 6 | 7 | import org.apache.zookeeper.KeeperException; 8 | import org.apache.zookeeper.KeeperException.NoNodeException; 9 | import org.apache.zookeeper.proto.ExistsRequest; 10 | import org.apache.zookeeper.proto.ExistsResponse; 11 | import org.apache.zookeeper.server.Request; 12 | 13 | import com.apple.foundationdb.Range; 14 | import com.apple.foundationdb.Transaction; 15 | import com.apple.foundationdb.directory.DirectoryLayer; 16 | import com.apple.foundationdb.directory.DirectorySubspace; 17 | import com.apple.foundationdb.directory.NoSuchDirectoryException; 18 | import com.google.inject.Inject; 19 | import com.hubspot.algebra.Result; 20 | import com.ph14.fdb.zk.FdbSchemaConstants; 21 | import com.ph14.fdb.zk.layer.FdbNode; 22 | import com.ph14.fdb.zk.layer.FdbNodeReader; 23 | import com.ph14.fdb.zk.layer.FdbPath; 24 | import com.ph14.fdb.zk.layer.FdbWatchManager; 25 | 26 | public class FdbExistsOp implements FdbOp { 27 | 28 | private final FdbNodeReader fdbNodeReader; 29 | private final FdbWatchManager fdbWatchManager; 30 | 31 | @Inject 32 | public FdbExistsOp(FdbNodeReader fdbNodeReader, 33 | FdbWatchManager fdbWatchManager) { 34 | this.fdbNodeReader = fdbNodeReader; 35 | this.fdbWatchManager = fdbWatchManager; 36 | } 37 | 38 | @Override 39 | public CompletableFuture> execute(Request zkRequest, Transaction transaction, ExistsRequest request) { 40 | List path = FdbPath.toFdbPath(request.getPath()); 41 | 42 | fdbWatchManager.checkForWatches(zkRequest.sessionId, zkRequest.cnxn); 43 | 44 | final DirectorySubspace subspace; 45 | try { 46 | subspace = DirectoryLayer.getDefault().open(transaction, path).join(); 47 | 48 | Range statKeyRange = subspace.get(FdbSchemaConstants.STAT_KEY).range(); 49 | 50 | FdbNode fdbNode = fdbNodeReader.getNode(subspace, transaction.getRange(statKeyRange).asList().join()); 51 | 52 | if (request.getWatch()) { 53 | fdbWatchManager.addNodeDataUpdatedWatch(transaction, request.getPath(), zkRequest.cnxn, zkRequest.sessionId); 54 | fdbWatchManager.addNodeDeletedWatch(transaction, request.getPath(), zkRequest.cnxn, zkRequest.sessionId); 55 | } 56 | 57 | return CompletableFuture.completedFuture(Result.ok(new ExistsResponse(fdbNode.getStat()))); 58 | } catch (CompletionException e) { 59 | if (e.getCause() instanceof NoSuchDirectoryException) { 60 | if (request.getWatch()) { 61 | fdbWatchManager.addNodeCreatedWatch(transaction, request.getPath(), zkRequest.cnxn, zkRequest.sessionId); 62 | } 63 | 64 | return CompletableFuture.completedFuture(Result.err(new NoNodeException(request.getPath()))); 65 | } else { 66 | throw new RuntimeException(e); 67 | } 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ops/FdbGetChildrenOp.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | import org.apache.zookeeper.KeeperException; 6 | import org.apache.zookeeper.proto.GetChildren2Request; 7 | import org.apache.zookeeper.proto.GetChildrenRequest; 8 | import org.apache.zookeeper.proto.GetChildrenResponse; 9 | import org.apache.zookeeper.server.Request; 10 | 11 | import com.apple.foundationdb.Transaction; 12 | import com.google.inject.Inject; 13 | import com.hubspot.algebra.Result; 14 | 15 | public class FdbGetChildrenOp implements FdbOp { 16 | 17 | private final FdbGetChildrenWithStatOp fdbGetChildrenWithStatOp; 18 | 19 | @Inject 20 | public FdbGetChildrenOp(FdbGetChildrenWithStatOp fdbGetChildrenWithStatOp) { 21 | this.fdbGetChildrenWithStatOp = fdbGetChildrenWithStatOp; 22 | } 23 | 24 | @Override 25 | public CompletableFuture> execute(Request zkRequest, Transaction transaction, GetChildrenRequest request) { 26 | return fdbGetChildrenWithStatOp.execute(zkRequest, transaction, new GetChildren2Request(request.getPath(), request.getWatch())) 27 | .thenApply(result -> result.mapOk(getChildren2Response -> new GetChildrenResponse(getChildren2Response.getChildren()))); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ops/FdbGetChildrenWithStatOp.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CompletableFuture; 5 | import java.util.concurrent.CompletionException; 6 | 7 | import org.apache.zookeeper.KeeperException; 8 | import org.apache.zookeeper.KeeperException.NoNodeException; 9 | import org.apache.zookeeper.data.Stat; 10 | import org.apache.zookeeper.proto.GetChildren2Request; 11 | import org.apache.zookeeper.proto.GetChildren2Response; 12 | import org.apache.zookeeper.server.Request; 13 | 14 | import com.apple.foundationdb.Transaction; 15 | import com.apple.foundationdb.directory.DirectoryLayer; 16 | import com.apple.foundationdb.directory.NoSuchDirectoryException; 17 | import com.google.inject.Inject; 18 | import com.hubspot.algebra.Result; 19 | import com.ph14.fdb.zk.layer.FdbNodeReader; 20 | import com.ph14.fdb.zk.layer.FdbPath; 21 | import com.ph14.fdb.zk.layer.FdbWatchManager; 22 | 23 | public class FdbGetChildrenWithStatOp implements FdbOp { 24 | 25 | private final FdbNodeReader fdbNodeReader; 26 | private final FdbWatchManager fdbWatchManager; 27 | 28 | @Inject 29 | public FdbGetChildrenWithStatOp(FdbNodeReader fdbNodeReader, 30 | FdbWatchManager fdbWatchManager) { 31 | this.fdbNodeReader = fdbNodeReader; 32 | this.fdbWatchManager = fdbWatchManager; 33 | } 34 | 35 | @Override 36 | public CompletableFuture> execute(Request zkRequest, Transaction transaction, GetChildren2Request request) { 37 | List path = FdbPath.toFdbPath(request.getPath()); 38 | 39 | fdbWatchManager.checkForWatches(zkRequest.sessionId, zkRequest.cnxn); 40 | 41 | try { 42 | Stat stat = fdbNodeReader.getNodeStat(DirectoryLayer.getDefault().open(transaction, path).join(), transaction).join(); 43 | List childrenDirectoryNames = DirectoryLayer.getDefault().list(transaction, path).join(); 44 | 45 | if (request.getWatch()) { 46 | fdbWatchManager.addNodeChildrenWatch(transaction, request.getPath(), zkRequest.cnxn, zkRequest.sessionId); 47 | fdbWatchManager.addNodeDeletedWatch(transaction, request.getPath(), zkRequest.cnxn, zkRequest.sessionId); 48 | } 49 | 50 | return CompletableFuture.completedFuture(Result.ok(new GetChildren2Response(childrenDirectoryNames, stat))); 51 | } catch (CompletionException e) { 52 | if (e.getCause() instanceof NoSuchDirectoryException) { 53 | return CompletableFuture.completedFuture(Result.err(new NoNodeException(request.getPath()))); 54 | } else { 55 | throw new RuntimeException(e); 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ops/FdbGetDataOp.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CompletableFuture; 5 | import java.util.concurrent.CompletionException; 6 | 7 | import org.apache.zookeeper.KeeperException; 8 | import org.apache.zookeeper.KeeperException.NoNodeException; 9 | import org.apache.zookeeper.proto.GetDataRequest; 10 | import org.apache.zookeeper.proto.GetDataResponse; 11 | import org.apache.zookeeper.server.Request; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import com.apple.foundationdb.Transaction; 16 | import com.apple.foundationdb.directory.DirectoryLayer; 17 | import com.apple.foundationdb.directory.DirectorySubspace; 18 | import com.apple.foundationdb.directory.NoSuchDirectoryException; 19 | import com.google.inject.Inject; 20 | import com.hubspot.algebra.Result; 21 | import com.ph14.fdb.zk.layer.FdbNode; 22 | import com.ph14.fdb.zk.layer.FdbNodeReader; 23 | import com.ph14.fdb.zk.layer.FdbPath; 24 | import com.ph14.fdb.zk.layer.FdbWatchManager; 25 | 26 | public class FdbGetDataOp implements FdbOp { 27 | 28 | private static final Logger LOG = LoggerFactory.getLogger(FdbGetDataOp.class); 29 | 30 | private final FdbNodeReader fdbNodeReader; 31 | private final FdbWatchManager fdbWatchManager; 32 | 33 | @Inject 34 | public FdbGetDataOp(FdbNodeReader fdbNodeReader, 35 | FdbWatchManager fdbWatchManager) { 36 | this.fdbNodeReader = fdbNodeReader; 37 | this.fdbWatchManager = fdbWatchManager; 38 | } 39 | 40 | @Override 41 | public CompletableFuture> execute(Request zkRequest, Transaction transaction, GetDataRequest request) { 42 | List path = FdbPath.toFdbPath(request.getPath()); 43 | 44 | fdbWatchManager.checkForWatches(zkRequest.sessionId, zkRequest.cnxn); 45 | 46 | final DirectorySubspace subspace; 47 | try { 48 | subspace = DirectoryLayer.getDefault().open(transaction, path).join(); 49 | } catch (CompletionException e) { 50 | if (e.getCause() instanceof NoSuchDirectoryException) { 51 | if (request.getWatch()) { 52 | LOG.info("Setting watch on node creation {}", request.getPath()); 53 | fdbWatchManager.addNodeCreatedWatch(transaction, request.getPath(), zkRequest.cnxn, zkRequest.sessionId); 54 | } 55 | 56 | return CompletableFuture.completedFuture(Result.err(new NoNodeException(request.getPath()))); 57 | } else { 58 | throw new RuntimeException(e); 59 | } 60 | } 61 | 62 | // we could be even more targeted and just fetch data 63 | CompletableFuture fdbNode = fdbNodeReader.getNode(subspace, transaction); 64 | 65 | if (request.getWatch()) { 66 | fdbWatchManager.addNodeDataUpdatedWatch(transaction, request.getPath(), zkRequest.cnxn, zkRequest.sessionId); 67 | fdbWatchManager.addNodeDeletedWatch(transaction, request.getPath(), zkRequest.cnxn, zkRequest.sessionId); 68 | } 69 | 70 | return CompletableFuture.completedFuture(Result.ok(new GetDataResponse(fdbNode.join().getData(), fdbNode.join().getStat()))); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ops/FdbMultiOp.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Iterator; 5 | import java.util.List; 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | import org.apache.zookeeper.KeeperException; 9 | import org.apache.zookeeper.MultiResponse; 10 | import org.apache.zookeeper.MultiTransactionRecord; 11 | import org.apache.zookeeper.Op; 12 | import org.apache.zookeeper.OpResult; 13 | import org.apache.zookeeper.OpResult.CheckResult; 14 | import org.apache.zookeeper.OpResult.CreateResult; 15 | import org.apache.zookeeper.OpResult.DeleteResult; 16 | import org.apache.zookeeper.OpResult.ErrorResult; 17 | import org.apache.zookeeper.OpResult.SetDataResult; 18 | import org.apache.zookeeper.ZooDefs.OpCode; 19 | import org.apache.zookeeper.proto.CheckVersionRequest; 20 | import org.apache.zookeeper.proto.CreateRequest; 21 | import org.apache.zookeeper.proto.DeleteRequest; 22 | import org.apache.zookeeper.proto.SetDataRequest; 23 | import org.apache.zookeeper.server.Request; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import com.apple.foundationdb.Database; 28 | import com.google.inject.Inject; 29 | import com.hubspot.algebra.Result; 30 | 31 | /** 32 | * Not functional yet... many of these ops need atomic mutations 33 | * which aren't read-your-writes friendly. 34 | * 35 | * Tracking: https://github.com/pH14/fdb-zk/issues/15 36 | */ 37 | public class FdbMultiOp { 38 | 39 | private static final Logger LOG = LoggerFactory.getLogger(FdbMultiOp.class); 40 | 41 | private final Database fdb; 42 | private final FdbCreateOp fdbCreateOp; 43 | private final FdbDeleteOp fdbDeleteOp; 44 | private final FdbSetDataOp fdbSetDataOp; 45 | private final FdbCheckVersionOp fdbCheckVersionOp; 46 | 47 | @Inject 48 | public FdbMultiOp(Database fdb, 49 | FdbCreateOp fdbCreateOp, 50 | FdbDeleteOp fdbDeleteOp, 51 | FdbSetDataOp fdbSetDataOp, 52 | FdbCheckVersionOp fdbCheckVersionOp) { 53 | this.fdb = fdb; 54 | this.fdbCreateOp = fdbCreateOp; 55 | this.fdbDeleteOp = fdbDeleteOp; 56 | this.fdbSetDataOp = fdbSetDataOp; 57 | this.fdbCheckVersionOp = fdbCheckVersionOp; 58 | } 59 | 60 | public MultiResponse execute(Request zkRequest, MultiTransactionRecord multiTransactionRecord) { 61 | List>> results = new ArrayList<>(); 62 | 63 | try { 64 | fdb.run(transaction -> { 65 | Iterator ops = multiTransactionRecord.iterator(); 66 | 67 | while (ops.hasNext()) { 68 | Op op = ops.next(); 69 | 70 | switch (op.getType()) { 71 | case OpCode.create: 72 | results.add( 73 | fdbCreateOp.execute(zkRequest, transaction, (CreateRequest) op.toRequestRecord()) 74 | .thenApply(r -> r.mapOk(response -> new CreateResult(response.getPath())))); 75 | break; 76 | case OpCode.delete: 77 | results.add( 78 | fdbDeleteOp.execute(zkRequest, transaction, (DeleteRequest) op.toRequestRecord()) 79 | .thenApply(r -> r.mapOk(response -> new DeleteResult()))); 80 | break; 81 | case OpCode.setData: 82 | results.add( 83 | fdbSetDataOp.execute(zkRequest, transaction, (SetDataRequest) op.toRequestRecord()) 84 | .thenApply(r -> r.mapOk(response -> new SetDataResult(response.getStat())))); 85 | break; 86 | case OpCode.check: 87 | results.add( 88 | fdbCheckVersionOp.execute(zkRequest, transaction, (CheckVersionRequest) op.toRequestRecord()) 89 | .thenApply(r -> r.mapOk(response -> new CheckResult()))); 90 | break; 91 | } 92 | } 93 | 94 | for (CompletableFuture> result : results) { 95 | Result individualResult = result.join(); 96 | if (individualResult.isErr()) { 97 | throw new AbortTransactionException(); 98 | } 99 | } 100 | 101 | return null; 102 | }); 103 | } catch (Exception e) { 104 | if (e.getCause() instanceof AbortTransactionException) { 105 | LOG.debug("Aborting transaction"); 106 | } else { 107 | throw e; 108 | } 109 | } 110 | 111 | MultiResponse multiResponse = new MultiResponse(); 112 | for (CompletableFuture> result : results) { 113 | Result individualResult = result.join(); 114 | 115 | if (individualResult.isErr()) { 116 | multiResponse.add(new ErrorResult(individualResult.unwrapErrOrElseThrow().code().intValue())); 117 | } else { 118 | multiResponse.add(individualResult.unwrapOrElseThrow()); 119 | } 120 | } 121 | 122 | return multiResponse; 123 | } 124 | 125 | 126 | private static class AbortTransactionException extends RuntimeException { 127 | public AbortTransactionException() { 128 | } 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ops/FdbOp.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | import org.apache.zookeeper.KeeperException; 6 | import org.apache.zookeeper.server.Request; 7 | 8 | import com.apple.foundationdb.Transaction; 9 | import com.hubspot.algebra.Result; 10 | 11 | public interface FdbOp { 12 | 13 | /** 14 | * Returned future must be joined after Transaction has committed and closed 15 | */ 16 | CompletableFuture> execute(Request zkRequest, Transaction transaction, REQ request); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ops/FdbSetDataOp.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CompletableFuture; 5 | import java.util.concurrent.CompletionException; 6 | 7 | import org.apache.zookeeper.KeeperException; 8 | import org.apache.zookeeper.KeeperException.APIErrorException; 9 | import org.apache.zookeeper.KeeperException.BadVersionException; 10 | import org.apache.zookeeper.KeeperException.NoNodeException; 11 | import org.apache.zookeeper.KeeperException.SystemErrorException; 12 | import org.apache.zookeeper.data.Stat; 13 | import org.apache.zookeeper.proto.SetDataRequest; 14 | import org.apache.zookeeper.proto.SetDataResponse; 15 | import org.apache.zookeeper.server.Request; 16 | 17 | import com.apple.foundationdb.Database; 18 | import com.apple.foundationdb.Transaction; 19 | import com.apple.foundationdb.directory.DirectoryLayer; 20 | import com.apple.foundationdb.directory.DirectorySubspace; 21 | import com.apple.foundationdb.directory.NoSuchDirectoryException; 22 | import com.apple.foundationdb.tuple.Versionstamp; 23 | import com.google.common.collect.ImmutableMap; 24 | import com.google.common.primitives.Longs; 25 | import com.google.inject.Inject; 26 | import com.hubspot.algebra.Result; 27 | import com.hubspot.algebra.Results; 28 | import com.ph14.fdb.zk.layer.FdbNodeReader; 29 | import com.ph14.fdb.zk.layer.FdbNodeWriter; 30 | import com.ph14.fdb.zk.layer.FdbPath; 31 | import com.ph14.fdb.zk.layer.FdbWatchManager; 32 | import com.ph14.fdb.zk.layer.StatKey; 33 | 34 | public class FdbSetDataOp implements FdbOp { 35 | 36 | private final FdbNodeWriter fdbNodeWriter; 37 | private final FdbNodeReader fdbNodeReader; 38 | private final FdbWatchManager fdbWatchManager; 39 | 40 | @Inject 41 | public FdbSetDataOp(FdbNodeReader fdbNodeReader, 42 | FdbNodeWriter fdbNodeWriter, 43 | FdbWatchManager fdbWatchManager) { 44 | this.fdbNodeReader = fdbNodeReader; 45 | this.fdbNodeWriter = fdbNodeWriter; 46 | this.fdbWatchManager = fdbWatchManager; 47 | } 48 | 49 | @Override 50 | public CompletableFuture> execute(Request zkRequest, Transaction transaction, SetDataRequest request) { 51 | List path = FdbPath.toFdbPath(request.getPath()); 52 | 53 | final DirectorySubspace subspace; 54 | final Stat stat; 55 | try { 56 | subspace = DirectoryLayer.getDefault().open(transaction, path).join(); 57 | stat = fdbNodeReader.getNodeStat(subspace, transaction).join(); 58 | 59 | if (stat.getVersion() != request.getVersion()) { 60 | return CompletableFuture.completedFuture(Result.err(new BadVersionException(request.getPath()))); 61 | } 62 | } catch (CompletionException e) { 63 | if (e.getCause() instanceof NoSuchDirectoryException) { 64 | return CompletableFuture.completedFuture(Results.err(new NoNodeException(request.getPath()))); 65 | } else { 66 | return CompletableFuture.completedFuture(Results.err(new APIErrorException())); 67 | } 68 | } 69 | 70 | fdbNodeWriter.writeData(transaction, subspace, request.getData()); 71 | 72 | fdbNodeWriter.writeStat(transaction, subspace, ImmutableMap.builder() 73 | .put(StatKey.MZXID, FdbNodeWriter.VERSIONSTAMP_FLAG) 74 | .put(StatKey.MTIME, System.currentTimeMillis()) 75 | .put(StatKey.VERSION, stat.getVersion() + 1L) 76 | .put(StatKey.DATA_LENGTH, (long) request.getData().length) 77 | .build()); 78 | 79 | fdbWatchManager.triggerNodeUpdatedWatch(transaction, request.getPath()); 80 | 81 | // This is a little messy... the ZK API returns the Stat of the node after modification. 82 | // The MZXID, the global transaction id at modification time, will be updated by the 83 | // transaction's `mutate` call, but since the mutate will apply the versionstamp 84 | // atomically once it hits the db, this means that we can't read-our-writes here and 85 | // we have to spin up a new transaction to observe it. 86 | // 87 | // In the new transaction read, we also don't want to return anything _more_ recent 88 | // than the commit made here so that it looks like it's part of the same ZK transaction, 89 | // so we need to record the commit id and set the read version to get exactly what 90 | // we just wrote. However... we don't know the version of the write until after it 91 | // commits, which is after this method is called, so we chain the read onto the commit 92 | // of the initial transaction. 93 | final Database database = transaction.getDatabase(); 94 | CompletableFuture commitVersionstamp = transaction.getVersionstamp(); 95 | 96 | return CompletableFuture.completedFuture( 97 | commitVersionstamp.handle((versionstamp, e) -> { 98 | if (e != null) { 99 | return Result.err(new SystemErrorException()); 100 | } 101 | 102 | return database.run(tr -> { 103 | long readVersionFromStamp = Longs.fromByteArray(Versionstamp.complete(versionstamp).getTransactionVersion()); 104 | tr.setReadVersion(readVersionFromStamp); 105 | 106 | Stat updatedStat = fdbNodeReader.getNodeStat(subspace, tr).join(); 107 | return Result.ok(new SetDataResponse(updatedStat)); 108 | }); 109 | })).join(); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/ops/FdbSetWatchesOp.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | import org.apache.zookeeper.KeeperException; 8 | import org.apache.zookeeper.WatchedEvent; 9 | import org.apache.zookeeper.Watcher; 10 | import org.apache.zookeeper.Watcher.Event.EventType; 11 | import org.apache.zookeeper.Watcher.Event.KeeperState; 12 | import org.apache.zookeeper.data.Stat; 13 | import org.apache.zookeeper.proto.SetWatches; 14 | import org.apache.zookeeper.server.Request; 15 | 16 | import com.apple.foundationdb.Transaction; 17 | import com.apple.foundationdb.async.AsyncUtil; 18 | import com.apple.foundationdb.directory.DirectoryLayer; 19 | import com.google.inject.Inject; 20 | import com.hubspot.algebra.Result; 21 | import com.ph14.fdb.zk.layer.FdbNodeReader; 22 | import com.ph14.fdb.zk.layer.FdbPath; 23 | import com.ph14.fdb.zk.layer.FdbWatchManager; 24 | import com.ph14.fdb.zk.layer.StatKey; 25 | 26 | public class FdbSetWatchesOp implements FdbOp { 27 | 28 | private final FdbNodeReader fdbNodeReader; 29 | private final FdbWatchManager fdbWatchManager; 30 | 31 | @Inject 32 | public FdbSetWatchesOp(FdbNodeReader fdbNodeReader, 33 | FdbWatchManager fdbWatchManager) { 34 | this.fdbNodeReader = fdbNodeReader; 35 | this.fdbWatchManager = fdbWatchManager; 36 | } 37 | 38 | @Override 39 | public CompletableFuture> execute(Request zkRequest, Transaction transaction, SetWatches setWatches) { 40 | Watcher watcher = zkRequest.cnxn; 41 | 42 | List> backfilledWatches = new ArrayList<>( 43 | setWatches.getDataWatches().size() 44 | + setWatches.getExistWatches().size() 45 | + setWatches.getChildWatches().size()); 46 | 47 | for (String path : setWatches.getDataWatches()) { 48 | backfilledWatches.add( 49 | readNodeStat(transaction, path, StatKey.MZXID) 50 | .thenAccept(stat -> { 51 | if (stat.getMzxid() == 0) { 52 | watcher.process(new WatchedEvent(EventType.NodeDeleted, 53 | KeeperState.SyncConnected, path)); 54 | } else if (stat.getMzxid() > setWatches.getRelativeZxid()) { 55 | watcher.process(new WatchedEvent(EventType.NodeDataChanged, 56 | KeeperState.SyncConnected, path)); 57 | } else { 58 | fdbWatchManager.addNodeDataUpdatedWatch(transaction, path, watcher, zkRequest.sessionId); 59 | } 60 | }) 61 | ); 62 | } 63 | 64 | for (String path : setWatches.getExistWatches()) { 65 | backfilledWatches.add( 66 | readNodeStat(transaction, path, StatKey.CZXID) 67 | .thenAccept(stat -> { 68 | if (stat.getCzxid() != 0) { 69 | watcher.process(new WatchedEvent(EventType.NodeCreated, 70 | KeeperState.SyncConnected, path)); 71 | } else { 72 | fdbWatchManager.addNodeCreatedWatch(transaction, path, watcher, zkRequest.sessionId); 73 | } 74 | }) 75 | ); 76 | } 77 | 78 | for (String path : setWatches.getChildWatches()) { 79 | backfilledWatches.add( 80 | readNodeStat(transaction, path, StatKey.PZXID) 81 | .thenAccept(stat -> { 82 | 83 | if (stat.getPzxid() == 0) { 84 | watcher.process(new WatchedEvent(EventType.NodeDeleted, 85 | KeeperState.SyncConnected, path)); 86 | } else if (stat.getPzxid() > setWatches.getRelativeZxid()) { 87 | watcher.process(new WatchedEvent(EventType.NodeChildrenChanged, 88 | KeeperState.SyncConnected, path)); 89 | } else { 90 | fdbWatchManager.addNodeChildrenWatch(transaction, path, watcher, zkRequest.sessionId); 91 | } 92 | }) 93 | ); 94 | } 95 | 96 | return AsyncUtil.whenAll(backfilledWatches).thenApply(v -> Result.ok(null)); 97 | } 98 | 99 | private CompletableFuture readNodeStat(Transaction transaction, String zkPath, StatKey ... statKeys) { 100 | List fdbPath = FdbPath.toFdbPath(zkPath); 101 | 102 | return DirectoryLayer.getDefault().open(transaction, fdbPath) 103 | .thenCompose(nodeSubspace -> fdbNodeReader.getNodeStat(nodeSubspace, transaction, statKeys)); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/session/CoordinatingClock.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.session; 2 | 3 | import java.io.Closeable; 4 | import java.time.Clock; 5 | import java.util.OptionalLong; 6 | import java.util.concurrent.Executors; 7 | import java.util.concurrent.ScheduledExecutorService; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import com.apple.foundationdb.Database; 15 | import com.apple.foundationdb.FDBException; 16 | import com.apple.foundationdb.Transaction; 17 | import com.apple.foundationdb.tuple.ByteArrayUtil; 18 | import com.apple.foundationdb.tuple.Tuple; 19 | import com.google.common.annotations.VisibleForTesting; 20 | import com.google.common.base.Charsets; 21 | import com.google.common.base.Preconditions; 22 | import com.google.common.base.Throwables; 23 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 24 | 25 | /** 26 | * Coordinating clock to pick a client to execute some function when elected. 27 | * 28 | * Each clock participant reads the clock key, which is a timestamp 29 | * of the end of the current tick. Clients wait until their local time ~= this value, 30 | * and then try to write ts + tick_interval. If all of their transactions were 31 | * open simultaneously, then only one write should win and the rest get conflicts. 32 | * The winning write is the "leader" until the next tick interval. 33 | * 34 | * This doesn't guarantee single-leaders, but ought to cull most clients each tick. 35 | */ 36 | public class CoordinatingClock implements Closeable { 37 | 38 | private static final Logger LOG = LoggerFactory.getLogger(CoordinatingClock.class); 39 | 40 | static final String PREFIX = "fdb-zk-clock"; 41 | static final byte[] KEY_PREFIX = Tuple.from(PREFIX).pack(); 42 | 43 | static final long DEFAULT_TICK_MILLIS = 1000; 44 | 45 | private final Database fdb; 46 | private final ScheduledExecutorService executorService; 47 | private final AtomicBoolean isClosed = new AtomicBoolean(false); 48 | private final String clockName; 49 | private final byte[] clockKey; 50 | private final Runnable onElection; 51 | private final Clock clock; 52 | private final long tickMillis; 53 | 54 | private CoordinatingClock(Database fdb, String clockName, Runnable onElection, Clock clock, OptionalLong tickMillis) { 55 | this.fdb = fdb; 56 | 57 | this.executorService = Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder() 58 | .setNameFormat("coordinating-clock-" + clockName) 59 | .setDaemon(true) 60 | .build()); 61 | 62 | this.clockName = clockName; 63 | this.clockKey = Tuple.from(PREFIX, clockName.getBytes(Charsets.UTF_8)).pack(); 64 | this.onElection = onElection; 65 | this.clock = clock; 66 | this.tickMillis = tickMillis.orElse(DEFAULT_TICK_MILLIS); 67 | } 68 | 69 | public static CoordinatingClock from(Database fdb, String clockName, Runnable onElection) { 70 | return new CoordinatingClock(fdb, clockName, onElection, Clock.systemUTC(), OptionalLong.empty()); 71 | } 72 | 73 | public static CoordinatingClock from(Database fdb, String clockName, Runnable onElection, Clock clock, OptionalLong tickMillis) { 74 | return new CoordinatingClock(fdb, clockName, onElection, clock, tickMillis); 75 | } 76 | 77 | public CoordinatingClock start() { 78 | Preconditions.checkState(!isClosed.get(), "cannot start clock, already closed"); 79 | 80 | executorService.scheduleAtFixedRate(() -> { 81 | try { 82 | if (!isClosed.get()) { 83 | keepTime(); 84 | } 85 | } catch (Exception e) { 86 | LOG.error("Error in coordinating clock", e); 87 | } 88 | }, 0, tickMillis, TimeUnit.MILLISECONDS); 89 | 90 | return this; 91 | } 92 | 93 | public OptionalLong getCurrentTick() { 94 | return fdb.read(rt -> { 95 | byte[] value = rt.get(clockKey).join(); 96 | 97 | if (value != null) { 98 | return OptionalLong.of(ByteArrayUtil.decodeInt(value)); 99 | } else { 100 | return OptionalLong.empty(); 101 | } 102 | }); 103 | } 104 | 105 | @VisibleForTesting 106 | void runOnce() { 107 | keepTime(); 108 | } 109 | 110 | @Override 111 | public void close() { 112 | isClosed.set(true); 113 | executorService.shutdown(); 114 | 115 | try { 116 | executorService.awaitTermination(10, TimeUnit.SECONDS); 117 | } catch (InterruptedException e) { 118 | Thread.currentThread().interrupt(); 119 | } 120 | } 121 | 122 | private void keepTime() { 123 | final long winningTick; 124 | 125 | try (Transaction transaction = fdb.createTransaction()) { 126 | byte[] currentTickToWaitFor = transaction.get(clockKey).join(); 127 | 128 | long now = clock.millis(); 129 | 130 | if (currentTickToWaitFor == null) { 131 | transaction.set(clockKey, ByteArrayUtil.encodeInt(roundToTick(now + tickMillis))); 132 | transaction.commit().join(); 133 | return; 134 | } 135 | 136 | long nextPersistedTick = roundToTick(ByteArrayUtil.decodeInt(currentTickToWaitFor) + tickMillis); 137 | 138 | boolean mustRetryBeforeEligibleForLeader = false; 139 | // our clock is ahead of the next tick, we'll try to advance it further 140 | if (now > nextPersistedTick) { 141 | nextPersistedTick = roundToTick(now + tickMillis); 142 | mustRetryBeforeEligibleForLeader = true; 143 | } else { 144 | // we're behind, so wait until we think our real-time is the next tick 145 | Thread.sleep(nextPersistedTick - now); 146 | } 147 | 148 | transaction.set(clockKey, ByteArrayUtil.encodeInt(nextPersistedTick)); 149 | transaction.commit().join(); 150 | 151 | winningTick = nextPersistedTick; 152 | 153 | if (mustRetryBeforeEligibleForLeader) { 154 | return; 155 | } 156 | } catch (Exception e) { 157 | for (Throwable throwable : Throwables.getCausalChain(e)) { 158 | if (throwable instanceof FDBException) { 159 | FDBException fdbException = (FDBException) throwable; 160 | if (fdbException.getCode() == 1020) { 161 | // this is a conflict exception, which is expected for roughly all but one client per tick 162 | return; 163 | } 164 | } 165 | } 166 | 167 | // we hit a real exception. we might want to apply backpressure here. 168 | // it's risky to entirely drop the loop, since a transient error that 169 | // impacts all clients could stop all ticks 170 | LOG.error("Exception hit in leader clock", e); 171 | return; 172 | } 173 | 174 | // we won! by which we mean we didn't get a conflict 175 | LOG.info("elected leader of {} clock @ {}", clockName, winningTick); 176 | 177 | onElection.run(); 178 | } 179 | 180 | private long roundToTick(long millis) { 181 | return tickMillis * (millis / tickMillis); 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/session/FdbSessionClock.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.session; 2 | 3 | import java.io.Closeable; 4 | import java.time.Clock; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.OptionalLong; 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import com.apple.foundationdb.Database; 14 | 15 | public class FdbSessionClock implements Closeable { 16 | 17 | private static final Logger LOG = LoggerFactory.getLogger(FdbSessionClock.class); 18 | 19 | private final Clock clock; 20 | private final FdbSessionManager fdbSessionManager; 21 | private final FdbSessionDataPurger fdbSessionDataPurger; 22 | private final CoordinatingClock coordinatingClock; 23 | 24 | public FdbSessionClock(Database fdb, 25 | Clock clock, 26 | OptionalLong serverTickMillis, 27 | FdbSessionManager fdbSessionManager, 28 | FdbSessionDataPurger fdbSessionDataPurger) { 29 | this.clock = clock; 30 | this.fdbSessionDataPurger = fdbSessionDataPurger; 31 | this.fdbSessionManager = fdbSessionManager; 32 | this.coordinatingClock = CoordinatingClock.from( 33 | fdb, 34 | "fdb-zk-session-cleanup", 35 | this::cleanExpiredSessions, 36 | clock, 37 | serverTickMillis); 38 | } 39 | 40 | public void run() { 41 | coordinatingClock.start(); 42 | } 43 | 44 | public void runOnce() { 45 | coordinatingClock.runOnce(); 46 | } 47 | 48 | @Override 49 | public void close() { 50 | coordinatingClock.close(); 51 | } 52 | 53 | private void cleanExpiredSessions() { 54 | List> futures = new ArrayList<>(); 55 | 56 | for (long expiredSessionId : fdbSessionManager.getExpiredSessionIds(clock.millis())) { 57 | futures.add(fdbSessionDataPurger.removeAllSessionData(expiredSessionId)); 58 | } 59 | 60 | futures.forEach(CompletableFuture::join); 61 | 62 | if (futures.size() > 0) { 63 | LOG.info("Cleared ephemeral nodes for {} sessions", futures.size()); 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/session/FdbSessionDataPurger.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.session; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | import org.apache.zookeeper.Watcher.Event.EventType; 8 | 9 | import com.apple.foundationdb.Database; 10 | import com.apple.foundationdb.async.AsyncUtil; 11 | import com.google.inject.Inject; 12 | import com.google.inject.Singleton; 13 | import com.ph14.fdb.zk.layer.FdbNodeReader; 14 | import com.ph14.fdb.zk.layer.FdbNodeWriter; 15 | import com.ph14.fdb.zk.layer.FdbPath; 16 | import com.ph14.fdb.zk.layer.changefeed.WatchEventChangefeed; 17 | import com.ph14.fdb.zk.layer.ephemeral.FdbEphemeralNodeManager; 18 | 19 | @Singleton 20 | public class FdbSessionDataPurger { 21 | 22 | private final Database fdb; 23 | private final WatchEventChangefeed watchEventChangefeed; 24 | private final FdbNodeWriter fdbNodeWriter; 25 | private final FdbNodeReader fdbNodeReader; 26 | private final FdbEphemeralNodeManager fdbEphemeralNodeManager; 27 | private final FdbSessionManager fdbSessionManager; 28 | 29 | @Inject 30 | public FdbSessionDataPurger(Database fdb, 31 | WatchEventChangefeed watchEventChangefeed, 32 | FdbNodeWriter fdbNodeWriter, 33 | FdbNodeReader fdbNodeReader, 34 | FdbEphemeralNodeManager fdbEphemeralNodeManager, 35 | FdbSessionManager fdbSessionManager) { 36 | this.fdb = fdb; 37 | this.watchEventChangefeed = watchEventChangefeed; 38 | this.fdbNodeWriter = fdbNodeWriter; 39 | this.fdbNodeReader = fdbNodeReader; 40 | this.fdbEphemeralNodeManager = fdbEphemeralNodeManager; 41 | this.fdbSessionManager = fdbSessionManager; 42 | } 43 | 44 | public CompletableFuture removeAllSessionData(long sessionId) { 45 | return fdb.runAsync(tr -> { 46 | List> deletions = new ArrayList<>(); 47 | 48 | deletions.add(watchEventChangefeed.clearAllWatchesForSession(tr, sessionId)); 49 | 50 | for (String zkPath : fdbEphemeralNodeManager.getEphemeralNodeZkPaths(tr, sessionId).join()) { 51 | deletions.add( 52 | fdbNodeReader.getNodeDirectory(tr, zkPath) 53 | .thenCompose(nodeDirectory -> fdbNodeWriter.deleteNodeAsync(tr, nodeDirectory))); 54 | // clearing the directory + handling changefeeds should share code with DeleteOp 55 | deletions.add(watchEventChangefeed.appendToChangefeed(tr, EventType.NodeDeleted, zkPath)); 56 | deletions.add(watchEventChangefeed.appendToChangefeed(tr, EventType.NodeChildrenChanged, FdbPath.toZkParentPath(zkPath))); 57 | } 58 | 59 | fdbEphemeralNodeManager.clearEphemeralNodesForSession(tr, sessionId); 60 | 61 | deletions.add(fdbSessionManager.removeSessionAsync(tr, sessionId)); 62 | 63 | return AsyncUtil.whenAll(deletions); 64 | }); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/ph14/fdb/zk/session/FdbSessionManager.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.session; 2 | 3 | import java.io.PrintWriter; 4 | import java.time.Clock; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.OptionalLong; 8 | import java.util.concurrent.CompletableFuture; 9 | import java.util.stream.Collectors; 10 | 11 | import org.apache.zookeeper.KeeperException.SessionExpiredException; 12 | import org.apache.zookeeper.KeeperException.SessionMovedException; 13 | import org.apache.zookeeper.server.SessionTracker; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import com.apple.foundationdb.Database; 18 | import com.apple.foundationdb.KeyValue; 19 | import com.apple.foundationdb.MutationType; 20 | import com.apple.foundationdb.Range; 21 | import com.apple.foundationdb.Transaction; 22 | import com.apple.foundationdb.directory.DirectoryLayer; 23 | import com.apple.foundationdb.directory.DirectorySubspace; 24 | import com.apple.foundationdb.tuple.Tuple; 25 | import com.apple.foundationdb.tuple.Versionstamp; 26 | import com.google.common.annotations.VisibleForTesting; 27 | import com.google.common.primitives.Bytes; 28 | import com.google.common.primitives.Longs; 29 | import com.google.common.primitives.Shorts; 30 | import com.google.inject.Inject; 31 | import com.google.inject.name.Named; 32 | 33 | public class FdbSessionManager implements SessionTracker { 34 | 35 | private static final Logger LOG = LoggerFactory.getLogger(FdbSessionManager.class); 36 | 37 | private static final byte[] EMPTY_VALUE = new byte[0]; 38 | 39 | static final List SESSION_DIRECTORY = Collections.singletonList("fdb-zk-sessions-by-id"); 40 | static final List SESSION_TIMEOUT_DIRECTORY = Collections.singletonList("fdb-zk-sessions-by-timeout"); 41 | 42 | private final Database fdb; 43 | private final byte[] incompleteSessionsByIdKey; 44 | private final DirectorySubspace sessionsById; 45 | private final DirectorySubspace sessionsByTimeout; 46 | private final Clock clock; 47 | private final long serverTickMillis; 48 | 49 | @Inject 50 | public FdbSessionManager(Database fdb, 51 | Clock clock, 52 | @Named("serverTickMillis") OptionalLong serverTickMillis) { 53 | this.fdb = fdb; 54 | this.sessionsById = fdb.run(tr -> DirectoryLayer.getDefault().createOrOpen(tr, SESSION_DIRECTORY).join()); 55 | this.sessionsByTimeout = fdb.run(tr -> DirectoryLayer.getDefault().createOrOpen(tr, SESSION_TIMEOUT_DIRECTORY).join()); 56 | this.incompleteSessionsByIdKey = sessionsById.packWithVersionstamp(Tuple.from(Versionstamp.incomplete())); 57 | this.clock = clock; 58 | this.serverTickMillis = serverTickMillis.orElse(500L); 59 | } 60 | 61 | @Override 62 | public long createSession(int sessionTimeout) { 63 | return fdb.run(tr -> { 64 | long sessionExpirationTimestamp = calculateSessionExpirationTimestamp(clock.millis(), sessionTimeout); 65 | 66 | // magic incantation for `FIRST_IN_BATCH` transaction option. 67 | // forces this transaction to get batch id == 0, so any competing 68 | // sessions are guaranteed to be unique by the transaction version 69 | // component of versionstamps alone 70 | tr.options().getOptionConsumer().setOption(710, null); 71 | 72 | // session id --> (next expiration, timeoutMs) 73 | tr.mutate( 74 | MutationType.SET_VERSIONSTAMPED_KEY, 75 | incompleteSessionsByIdKey, 76 | getSessionByIdValue(sessionExpirationTimestamp, sessionTimeout)); 77 | 78 | // next expiration --> session ids 79 | tr.mutate( 80 | MutationType.SET_VERSIONSTAMPED_KEY, 81 | getIncompleteSessionByTimestampKey(sessionExpirationTimestamp), 82 | EMPTY_VALUE); 83 | 84 | return tr.getVersionstamp(); 85 | }) 86 | .thenApply(Versionstamp::complete) 87 | .thenApply(Versionstamp::getTransactionVersion) 88 | .thenApply(Longs::fromByteArray) 89 | .join(); 90 | } 91 | 92 | @Override 93 | public void addSession(long sessionId, int sessionTimeout) { 94 | long sessionExpirationTimestamp = calculateSessionExpirationTimestamp(clock.millis(), sessionTimeout); 95 | 96 | fdb.run(tr -> { 97 | byte[] sessionByIdKey = getSessionByIdKey(sessionId); 98 | 99 | tr.get(sessionByIdKey).thenAccept(currentSession -> { 100 | if (currentSession == null) { 101 | // session id --> next expiration 102 | tr.set( 103 | sessionByIdKey, 104 | getSessionByIdValue(sessionExpirationTimestamp, sessionTimeout)); 105 | 106 | // next expiration --> session ids 107 | tr.set( 108 | getSessionByTimestampKey(sessionId, sessionExpirationTimestamp), 109 | EMPTY_VALUE); 110 | } 111 | }).join(); 112 | 113 | return null; 114 | }); 115 | 116 | touchSession(sessionId, sessionTimeout); 117 | } 118 | 119 | @Override 120 | public boolean touchSession(long sessionId, int sessionTimeout) { 121 | byte[] value = fdb.read(rt -> rt.get(getSessionByIdKey(sessionId))).join(); 122 | 123 | if (value == null) { 124 | return false; 125 | } 126 | 127 | Tuple tuple = Tuple.fromBytes(value); 128 | long persistedNextSessionExpirationTimestamp = tuple.getLong(0); 129 | 130 | long newlyComputedNextExpirationTimestamp = calculateSessionExpirationTimestamp(clock.millis(), sessionTimeout); 131 | 132 | if (persistedNextSessionExpirationTimestamp >= newlyComputedNextExpirationTimestamp) { 133 | // this session already got a higher expiration timestamp from a previous touch with a greater `sessionTimeout` 134 | return true; 135 | } 136 | 137 | return fdb.run(tr -> { 138 | // update existing session --> timestamp entry 139 | tr.set( 140 | getSessionByIdKey(sessionId), 141 | getSessionByIdValue(newlyComputedNextExpirationTimestamp, sessionTimeout) 142 | ); 143 | 144 | // update timestamp --> session 145 | tr.clear(getSessionByTimestampKey(sessionId, persistedNextSessionExpirationTimestamp)); 146 | tr.set( 147 | getSessionByTimestampKey(sessionId, newlyComputedNextExpirationTimestamp), 148 | EMPTY_VALUE 149 | ); 150 | 151 | return true; 152 | }); 153 | } 154 | 155 | @Override 156 | public void setSessionClosing(long sessionId) { 157 | removeSession(sessionId); 158 | } 159 | 160 | @Override 161 | public void removeSession(long sessionId) { 162 | fdb.run(tr -> removeSessionAsync(tr, sessionId)).join(); 163 | } 164 | 165 | public CompletableFuture removeSessionAsync(Transaction transaction, long sessionId) { 166 | byte[] sessionByIdKey = getSessionByIdKey(sessionId); 167 | 168 | return transaction.get(sessionByIdKey).thenAccept(currentSession -> { 169 | if (currentSession != null) { 170 | transaction.clear(sessionByIdKey); 171 | long sessionExpirationTimestamp = Tuple.fromBytes(currentSession).getLong(0); 172 | 173 | // next expiration --> session ids 174 | transaction.clear(getSessionByTimestampKey(sessionId, sessionExpirationTimestamp)); 175 | } 176 | }); 177 | } 178 | 179 | @Override 180 | public void shutdown() { 181 | // N/A -- state stored in FDB, nothing to clean up here 182 | } 183 | 184 | @Override 185 | public void checkSession(long sessionId, Object owner) throws SessionExpiredException, SessionMovedException { 186 | byte[] value = fdb.read(rt -> rt.get(getSessionByIdKey(sessionId)).join()); 187 | 188 | if (value == null) { 189 | throw new SessionExpiredException(); 190 | } 191 | 192 | Tuple tuple = Tuple.fromBytes(value); 193 | long sessionExpirationTimestamp = tuple.getLong(0); 194 | 195 | if (sessionExpirationTimestamp <= clock.millis()) { 196 | throw new SessionExpiredException(); 197 | } 198 | } 199 | 200 | @VisibleForTesting 201 | Tuple getSessionDataById(long sessionId) { 202 | return Tuple.fromBytes(fdb.read(rt -> rt.get(getSessionByIdKey(sessionId)).join())); 203 | } 204 | 205 | public List getExpiredSessionIds(long greatestExpiredSessionTimestamp) { 206 | List keyValues = fdb.read(rt -> rt.getRange( 207 | new Range( 208 | getSessionByTimestampKey(0, 0), 209 | getSessionByTimestampKey(Long.MAX_VALUE, greatestExpiredSessionTimestamp))) 210 | .asList() 211 | .join() 212 | ); 213 | 214 | return keyValues.stream() 215 | .map(kv -> Tuple.fromBytes(kv.getKey()).getVersionstamp(2).getTransactionVersion()) 216 | .map(Longs::fromByteArray) 217 | .collect(Collectors.toList()); 218 | } 219 | 220 | @Override 221 | public void setOwner(long id, Object owner) { 222 | // not needed, state is stored in fdb 223 | } 224 | 225 | @Override 226 | public void dumpSessions(PrintWriter pwriter) { 227 | } 228 | 229 | private byte[] getSessionByIdKey(long sessionId) { 230 | byte[] versionstampBytes = Bytes.concat(Longs.toByteArray(sessionId), Shorts.toByteArray((short) 0)); 231 | return sessionsById.pack(Tuple.from(Versionstamp.complete(versionstampBytes))); 232 | } 233 | 234 | private byte[] getSessionByIdValue(long sessionExpirationTimestamp, long sessionTimeoutMillis) { 235 | return Tuple.from(sessionExpirationTimestamp, sessionTimeoutMillis).pack(); 236 | } 237 | 238 | private byte[] getSessionByTimestampKey(long sessionId, long sessionExpirationTimestamp) { 239 | byte[] versionstampBytes = Bytes.concat(Longs.toByteArray(sessionId), Shorts.toByteArray((short) 0)); 240 | return sessionsByTimeout.pack(Tuple.from(sessionExpirationTimestamp, Versionstamp.complete(versionstampBytes))); 241 | } 242 | 243 | private byte[] getIncompleteSessionByTimestampKey(long sessionExpirationTimestamp) { 244 | return sessionsByTimeout.packWithVersionstamp(Tuple.from(sessionExpirationTimestamp, Versionstamp.incomplete())); 245 | } 246 | 247 | private long calculateSessionExpirationTimestamp(long now, int timeoutMillis) { 248 | // We give a one interval grace period 249 | return ((now + timeoutMillis) / serverTickMillis + 1) * serverTickMillis; 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=DEBUG, stdout 2 | 3 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 4 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 5 | 6 | # Pattern to output the caller's file name and line number. 7 | log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n 8 | 9 | log4j.appender.R=org.apache.log4j.RollingFileAppender 10 | log4j.appender.R.File=example.log 11 | 12 | log4j.appender.R.MaxFileSize=100KB 13 | # Keep one backup file 14 | log4j.appender.R.MaxBackupIndex=1 15 | 16 | log4j.appender.R.layout=org.apache.log4j.PatternLayout 17 | log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n 18 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/ByteUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.List; 6 | 7 | import org.junit.Test; 8 | 9 | public class ByteUtilTest { 10 | 11 | @Test 12 | public void itSplitsByteArrays() { 13 | List bytes = ByteUtil.divideByteArray(new byte[] { 0x13, 0x32 }, 1); 14 | assertThat(bytes).containsExactly(new byte[] { 0x13 }, new byte[] { 0x32 }); 15 | 16 | bytes = ByteUtil.divideByteArray(new byte[] { 0x13, 0x32 }, 2); 17 | assertThat(bytes).containsExactly(new byte[] { 0x13, 0x32 }); 18 | 19 | bytes = ByteUtil.divideByteArray(new byte[] { 0x13, 0x32, 0x11 }, 2); 20 | assertThat(bytes).containsExactly(new byte[] { 0x13, 0x32 }, new byte[] { 0x11 }); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/FdbBaseTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk; 2 | 3 | import java.util.Collections; 4 | 5 | import org.apache.zookeeper.ZooDefs.Ids; 6 | import org.apache.zookeeper.server.MockFdbServerCnxn; 7 | import org.apache.zookeeper.server.Request; 8 | import org.junit.After; 9 | import org.junit.Before; 10 | 11 | import com.apple.foundationdb.Database; 12 | import com.apple.foundationdb.FDB; 13 | import com.apple.foundationdb.Transaction; 14 | import com.apple.foundationdb.directory.DirectoryLayer; 15 | import com.apple.foundationdb.directory.DirectorySubspace; 16 | import com.ph14.fdb.zk.layer.FdbNode; 17 | import com.ph14.fdb.zk.layer.FdbNodeReader; 18 | import com.ph14.fdb.zk.layer.FdbNodeWriter; 19 | import com.ph14.fdb.zk.layer.FdbPath; 20 | import com.ph14.fdb.zk.layer.FdbWatchManager; 21 | import com.ph14.fdb.zk.layer.changefeed.WatchEventChangefeed; 22 | import com.ph14.fdb.zk.layer.ephemeral.FdbEphemeralNodeManager; 23 | import com.ph14.fdb.zk.ops.FdbCheckVersionOp; 24 | import com.ph14.fdb.zk.ops.FdbCreateOp; 25 | import com.ph14.fdb.zk.ops.FdbDeleteOp; 26 | import com.ph14.fdb.zk.ops.FdbExistsOp; 27 | import com.ph14.fdb.zk.ops.FdbGetChildrenOp; 28 | import com.ph14.fdb.zk.ops.FdbGetChildrenWithStatOp; 29 | import com.ph14.fdb.zk.ops.FdbGetDataOp; 30 | import com.ph14.fdb.zk.ops.FdbMultiOp; 31 | import com.ph14.fdb.zk.ops.FdbSetDataOp; 32 | 33 | public class FdbBaseTest { 34 | 35 | protected static final String BASE_PATH = "/foo"; 36 | protected static final String SUBPATH = "/foo/bar"; 37 | protected static final MockFdbServerCnxn SERVER_CNXN = new MockFdbServerCnxn(); 38 | protected static Request REQUEST = new Request(SERVER_CNXN, System.currentTimeMillis(), 1, 2, null, Collections.emptyList()); 39 | 40 | protected FdbNodeWriter fdbNodeWriter; 41 | protected FdbWatchManager fdbWatchManager; 42 | protected WatchEventChangefeed watchEventChangefeed; 43 | protected FdbNodeReader fdbNodeReader; 44 | protected FdbEphemeralNodeManager fdbEphemeralNodeManager; 45 | 46 | protected FdbCreateOp fdbCreateOp; 47 | protected FdbGetDataOp fdbGetDataOp; 48 | protected FdbSetDataOp fdbSetDataOp; 49 | protected FdbExistsOp fdbExistsOp; 50 | protected FdbGetChildrenOp fdbGetChildrenOp; 51 | protected FdbGetChildrenWithStatOp fdbGetChildrenWithStatOp; 52 | protected FdbDeleteOp fdbDeleteOp; 53 | protected FdbCheckVersionOp fdbCheckVersionOp; 54 | protected FdbMultiOp fdbMultiOp; 55 | 56 | protected Database fdb; 57 | protected Transaction transaction; 58 | 59 | @Before 60 | public void setUp() { 61 | this.fdb = FDB.selectAPIVersion(600).open(); 62 | 63 | SERVER_CNXN.clearWatchedEvents(); 64 | REQUEST = new Request(SERVER_CNXN, System.nanoTime(), 1, 2, null, Collections.emptyList()); 65 | 66 | fdbNodeWriter = new FdbNodeWriter(); 67 | watchEventChangefeed = new WatchEventChangefeed(fdb); 68 | fdbWatchManager = new FdbWatchManager(watchEventChangefeed); 69 | fdbNodeReader = new FdbNodeReader(); 70 | fdbEphemeralNodeManager = new FdbEphemeralNodeManager(); 71 | 72 | fdbCreateOp = new FdbCreateOp(fdbNodeReader, fdbNodeWriter, fdbWatchManager, fdbEphemeralNodeManager); 73 | fdbGetDataOp = new FdbGetDataOp(fdbNodeReader, fdbWatchManager); 74 | fdbSetDataOp = new FdbSetDataOp(fdbNodeReader, fdbNodeWriter, fdbWatchManager); 75 | fdbExistsOp = new FdbExistsOp(fdbNodeReader, fdbWatchManager); 76 | fdbExistsOp = new FdbExistsOp(fdbNodeReader, fdbWatchManager); 77 | fdbGetChildrenWithStatOp = new FdbGetChildrenWithStatOp(fdbNodeReader, fdbWatchManager); 78 | fdbGetChildrenOp = new FdbGetChildrenOp(fdbGetChildrenWithStatOp); 79 | fdbDeleteOp = new FdbDeleteOp(fdbNodeReader, fdbNodeWriter, fdbWatchManager, fdbEphemeralNodeManager); 80 | fdbCheckVersionOp = new FdbCheckVersionOp(fdbNodeReader); 81 | fdbMultiOp = new FdbMultiOp(fdb, fdbCreateOp, fdbDeleteOp, fdbSetDataOp, fdbCheckVersionOp); 82 | 83 | fdb.run(tr -> { 84 | DirectoryLayer.getDefault().removeIfExists(tr, Collections.singletonList(FdbPath.ROOT_PATH)).join(); 85 | DirectorySubspace rootSubspace = DirectoryLayer.getDefault().create(tr, Collections.singletonList(FdbPath.ROOT_PATH)).join(); 86 | 87 | fdbNodeWriter.createNewNode(tr, rootSubspace, new FdbNode("/", null, new byte[0], Ids.OPEN_ACL_UNSAFE)); 88 | 89 | return null; 90 | }); 91 | 92 | this.transaction = fdb.createTransaction(); 93 | } 94 | 95 | @After 96 | public void tearDown() { 97 | this.transaction.cancel(); 98 | this.fdb.close(); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/FdbZkServerTestUtil.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk; 2 | 3 | import java.io.File; 4 | import java.net.InetSocketAddress; 5 | 6 | import org.apache.zookeeper.server.NIOServerCnxnFactory; 7 | import org.apache.zookeeper.server.ZooKeeperServer; 8 | 9 | public class FdbZkServerTestUtil { 10 | 11 | public static ZooKeeperServer newFdbZkServer(int port) throws Exception { 12 | int numConnections = 5000; 13 | int tickTime = 2000; 14 | String dataDirectory = System.getProperty("java.io.tmpdir"); 15 | 16 | File dir = new File(dataDirectory).getAbsoluteFile(); 17 | 18 | ZooKeeperServer server = new FdbZooKeeperServer(tickTime); 19 | NIOServerCnxnFactory standaloneServerFactory = new NIOServerCnxnFactory(); 20 | 21 | standaloneServerFactory.configure(new InetSocketAddress(port), numConnections); 22 | standaloneServerFactory.startup(server); 23 | 24 | return server; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/LocalRealZooKeeperScratchTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk; 2 | 3 | import java.io.File; 4 | import java.net.InetSocketAddress; 5 | import java.util.List; 6 | 7 | import org.apache.zookeeper.CreateMode; 8 | import org.apache.zookeeper.WatchedEvent; 9 | import org.apache.zookeeper.Watcher; 10 | import org.apache.zookeeper.ZooDefs.Ids; 11 | import org.apache.zookeeper.ZooKeeper; 12 | import org.apache.zookeeper.server.NIOServerCnxnFactory; 13 | import org.apache.zookeeper.server.ServerCnxn; 14 | import org.apache.zookeeper.server.ZooKeeperServer; 15 | import org.junit.Test; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | public class LocalRealZooKeeperScratchTest { 20 | 21 | @Test 22 | public void itRunsInProcess() throws Exception { 23 | int clientPort = 21818; // none-standard 24 | int numConnections = 5000; 25 | int tickTime = 2000; 26 | String dataDirectory = System.getProperty("java.io.tmpdir"); 27 | 28 | File dir = new File(dataDirectory).getAbsoluteFile(); 29 | 30 | ZooKeeperServer server = new ZooKeeperServer(dir, dir, 100); 31 | NIOServerCnxnFactory standaloneServerFactory = new NIOServerCnxnFactory(); 32 | standaloneServerFactory.configure(new InetSocketAddress(clientPort), numConnections); 33 | 34 | standaloneServerFactory.startup(server); // start the server. 35 | 36 | ZooKeeper zooKeeper = new ZooKeeper("localhost:21818", 10000, new Watcher() { 37 | public void process(WatchedEvent event) { 38 | LOG.info("Watched event: {}", event.toString()); 39 | } 40 | }); 41 | 42 | while (!zooKeeper.getState().isConnected()) { 43 | } 44 | 45 | System.out.println("Server state: " + server.serverStats()); 46 | System.out.println("Local hostname: " + standaloneServerFactory.getLocalAddress().getAddress().getHostName()); 47 | 48 | System.out.println("Connected: " + zooKeeper.getState().isConnected()); 49 | System.out.println("Alive: " + zooKeeper.getState().isAlive()); 50 | 51 | for (ServerCnxn connection : standaloneServerFactory.getConnections()) { 52 | System.out.println("Connections: " + connection.toString()); 53 | } 54 | 55 | String root = "/" + String.valueOf(System.currentTimeMillis()); 56 | 57 | zooKeeper.create(root, "hello".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); 58 | zooKeeper.create(root + "/abc", "hello".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL); 59 | // zooKeeper.create(root, "hello".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL); 60 | // LOG.info("CVersion before anything: {}", zooKeeper.exists(root, false).getCversion()); 61 | // LOG.info("PZXID before anything: {}", zooKeeper.exists(root, false).getPzxid()); 62 | // zooKeeper.create(root + "/1", "hello".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); 63 | // LOG.info("CVersion after 1: {}", zooKeeper.exists(root, false).getCversion()); 64 | // zooKeeper.create(root + "/2", "hello".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); 65 | // LOG.info("CVersion after 2: {}", zooKeeper.exists(root, false).getCversion()); 66 | List keeperChildren = zooKeeper.getChildren("/", false); 67 | LOG.info("Keep children: {}", keeperChildren); 68 | 69 | // LOG.info("CVersion after creations: {}", zooKeeper.exists(root, false).getCversion()); 70 | // LOG.info("CVersion after creations of /1: {}", zooKeeper.exists(root + "/1", false).getCversion()); 71 | // zooKeeper.delete(root + "/1", 0); 72 | // LOG.info("CVersion after deletion too: {}", zooKeeper.exists(root, false).getCversion()); 73 | 74 | // Stat exists = zooKeeper.exists("/start0000000001", false); 75 | // System.out.println("Exists: " + exists); 76 | // exists = zooKeeper.exists("/start0000000002", false); 77 | // System.out.println("Exists: " + exists); 78 | // exists = zooKeeper.exists("/start0000000003", false); 79 | // System.out.println("Exists: " + exists); 80 | // exists = zooKeeper.exists("/", false); 81 | // System.out.println("Exists: " + exists); 82 | 83 | List children = zooKeeper.getChildren("/", false); 84 | LOG.info("Root level children: {}", children); 85 | 86 | standaloneServerFactory.closeAll(); 87 | } 88 | 89 | private static final Logger LOG = LoggerFactory.getLogger(LocalRealZooKeeperScratchTest.class); 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/curator/LeaderCandidate.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.curator; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CountDownLatch; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import org.apache.curator.framework.CuratorFramework; 8 | import org.apache.curator.framework.CuratorFrameworkFactory; 9 | import org.apache.curator.framework.recipes.leader.LeaderLatch; 10 | import org.apache.curator.framework.recipes.leader.LeaderLatchListener; 11 | import org.apache.curator.retry.RetryNTimes; 12 | import org.apache.zookeeper.server.ZooKeeperServer; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import com.ph14.fdb.zk.FdbZkServerTestUtil; 17 | 18 | public class LeaderCandidate { 19 | 20 | private static final Logger LOG = LoggerFactory.getLogger(LeaderCandidate.class); 21 | 22 | public void run(int id, int numParticipants, List numberOfParticipantsPerRound) throws Exception { 23 | int port = 21818 + id; 24 | ZooKeeperServer zooKeeperServer = FdbZkServerTestUtil.newFdbZkServer(port); 25 | 26 | CuratorFramework curator = CuratorFrameworkFactory.newClient("localhost:" + port, 10000, 5000, new RetryNTimes(10, 10)); 27 | curator.start(); 28 | curator.blockUntilConnected(); 29 | 30 | long sessionId = curator.getZookeeperClient().getZooKeeper().getSessionId(); 31 | 32 | LeaderLatch leaderLatch = new LeaderLatch(curator, "/leader-latch"); 33 | 34 | CountDownLatch isLeaderCountdownLatch = new CountDownLatch(1); 35 | leaderLatch.addListener(new LeaderLatchListener() { 36 | @Override 37 | public void isLeader() { 38 | LOG.info("Session: {} became the leader", sessionId); 39 | isLeaderCountdownLatch.countDown(); 40 | } 41 | 42 | @Override 43 | public void notLeader() { 44 | throw new IllegalStateException(""); 45 | } 46 | }); 47 | 48 | leaderLatch.start(); 49 | 50 | while (leaderLatch.getParticipants().size() < numParticipants) { 51 | Thread.sleep(10); 52 | } 53 | 54 | // kill once elected leader to reduce participants by 1 55 | if (leaderLatch.await(10, TimeUnit.SECONDS)) { 56 | LOG.info("Session {} elected leader. {} participants", sessionId, leaderLatch.getParticipants().size()); 57 | numberOfParticipantsPerRound.add(leaderLatch.getParticipants().size()); 58 | 59 | leaderLatch.close(); 60 | zooKeeperServer.shutdown(); 61 | return; 62 | } 63 | 64 | throw new IllegalStateException("should have been elected leader"); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/curator/LeaderElection.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.curator; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.concurrent.ExecutorService; 9 | import java.util.concurrent.Executors; 10 | 11 | import org.junit.Test; 12 | 13 | public class LeaderElection { 14 | 15 | @Test 16 | public void itRunsALeaderElection() { 17 | int numberOfParticipants = 15; 18 | 19 | ExecutorService executorService = Executors.newFixedThreadPool(numberOfParticipants); 20 | 21 | List> futures = new ArrayList<>(); 22 | List numberOfParticipantsPerElectionLog = new ArrayList<>(); 23 | 24 | for (int i = 0; i < numberOfParticipants; i++) { 25 | final int id = i; 26 | CompletableFuture future = CompletableFuture.runAsync(() -> { 27 | try { 28 | new LeaderCandidate().run(id, numberOfParticipants, numberOfParticipantsPerElectionLog); 29 | } catch (Exception e) { 30 | throw new RuntimeException(e); 31 | } 32 | }, executorService); 33 | 34 | futures.add(future); 35 | } 36 | 37 | futures.forEach(CompletableFuture::join); 38 | 39 | assertThat(numberOfParticipantsPerElectionLog).containsExactly(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/layer/FdbNodeSerialization.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.layer; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | import org.apache.zookeeper.data.ACL; 9 | import org.apache.zookeeper.data.Id; 10 | import org.apache.zookeeper.data.Stat; 11 | import org.junit.Test; 12 | 13 | import com.apple.foundationdb.KeyValue; 14 | import com.apple.foundationdb.directory.DirectoryLayer; 15 | import com.apple.foundationdb.directory.DirectorySubspace; 16 | import com.google.common.base.Strings; 17 | import com.ph14.fdb.zk.FdbBaseTest; 18 | 19 | public class FdbNodeSerialization extends FdbBaseTest { 20 | 21 | @Test 22 | public void itWritesAndReadsFdbNodes() { 23 | String path = "/foo/bar/abd/wow"; 24 | 25 | FdbNode fdbNode = new FdbNode( 26 | path, 27 | new Stat(123L, 456L, System.currentTimeMillis(), Long.MAX_VALUE - System.currentTimeMillis(), 1337, 7331, 9001, 1L, 2, 3, 0L), 28 | Strings.repeat("hello this is a data block isn't that neat?", 10000).getBytes(), 29 | Arrays.asList( 30 | new ACL(123, new Id("a schema", "id!")), 31 | new ACL(456, new Id("another schema", "!id")) 32 | )); 33 | 34 | DirectorySubspace subspace = DirectoryLayer.getDefault().create(transaction, fdbNode.getFdbPath()).join(); 35 | 36 | fdbNodeWriter.createNewNode(transaction, subspace, fdbNode); 37 | 38 | List keyValues = transaction.getRange(subspace.range()).asList().join(); 39 | 40 | FdbNode fetchedFdbNode = fdbNodeReader.getNode(subspace, keyValues); 41 | 42 | assertThat(fetchedFdbNode).isEqualToComparingFieldByField(fdbNode); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/layer/FdbPathTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.layer; 2 | 3 | import static com.ph14.fdb.zk.layer.FdbPath.ROOT_PATH; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import java.util.List; 7 | 8 | import org.junit.Test; 9 | 10 | import com.google.common.collect.ImmutableList; 11 | 12 | public class FdbPathTest { 13 | 14 | @Test 15 | public void itConvertsToFdbPath() { 16 | List path = FdbPath.toFdbPath("/abc/foo/bar"); 17 | assertThat(path).containsExactly(ROOT_PATH, "abc", "foo", "bar"); 18 | } 19 | 20 | @Test 21 | public void itConvertsToFdbParentPath() { 22 | List path = FdbPath.toFdbParentPath("/abc/foo/bar"); 23 | assertThat(path).containsExactly(ROOT_PATH, "abc", "foo"); 24 | } 25 | 26 | @Test 27 | public void itConvertsRoot() { 28 | List path = FdbPath.toFdbPath("/"); 29 | assertThat(path).containsExactly(ROOT_PATH); 30 | } 31 | 32 | @Test 33 | public void itConvertsBackAndForth() { 34 | assertThat(FdbPath.toZkPath(FdbPath.toFdbPath("/abc/foo/bar"))).isEqualTo("/abc/foo/bar"); 35 | } 36 | 37 | @Test 38 | public void itConvertsParentPathBackAndForth() { 39 | assertThat(FdbPath.toZkPath(FdbPath.toFdbParentPath("/abc/foo/bar"))).isEqualTo("/abc/foo"); 40 | } 41 | 42 | @Test 43 | public void itConvertsToZkPath() { 44 | String path = FdbPath.toZkPath(ImmutableList.of(ROOT_PATH, "abc", "foo", "bar")); 45 | assertThat(path).isEqualTo("/abc/foo/bar"); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/ops/FdbCreateOpTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.Collections; 6 | import java.util.concurrent.CountDownLatch; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import org.apache.zookeeper.CreateMode; 10 | import org.apache.zookeeper.KeeperException; 11 | import org.apache.zookeeper.KeeperException.Code; 12 | import org.apache.zookeeper.Watcher; 13 | import org.apache.zookeeper.Watcher.Event.EventType; 14 | import org.apache.zookeeper.proto.CreateRequest; 15 | import org.apache.zookeeper.proto.CreateResponse; 16 | import org.apache.zookeeper.proto.ExistsRequest; 17 | import org.apache.zookeeper.proto.ExistsResponse; 18 | import org.junit.Test; 19 | 20 | import com.hubspot.algebra.Result; 21 | import com.ph14.fdb.zk.FdbBaseTest; 22 | 23 | public class FdbCreateOpTest extends FdbBaseTest { 24 | 25 | @Test 26 | public void itCreatesADirectory() { 27 | Result result = fdb.run( 28 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 29 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 30 | } 31 | 32 | @Test 33 | public void itDoesNotCreateTheSameDirectoryTwice() { 34 | Result result = fdb.run( 35 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 36 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 37 | 38 | result = fdb.run( 39 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 40 | assertThat(result.unwrapErrOrElseThrow().code()).isEqualTo(Code.NODEEXISTS); 41 | } 42 | 43 | @Test 44 | public void itDoesNotCreateDirectoryWithoutParent() { 45 | Result result = fdb.run( 46 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(SUBPATH, new byte[0], Collections.emptyList(), 0))).join(); 47 | assertThat(result.unwrapErrOrElseThrow().code()).isEqualTo(Code.NONODE); 48 | } 49 | 50 | @Test 51 | public void itProgressivelyCreatesNodes() { 52 | Result result = fdb.run( 53 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 54 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 55 | 56 | result = fdb.run( 57 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(SUBPATH, new byte[0], Collections.emptyList(), 0))).join(); 58 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(SUBPATH)); 59 | } 60 | 61 | @Test 62 | public void itUpdatesParentNodeVersionAndChildrenCount() { 63 | Result parent = fdb.run(tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest("/", false))).join(); 64 | assertThat(parent.unwrapOrElseThrow().getStat().getCversion()).isEqualTo(0); 65 | assertThat(parent.unwrapOrElseThrow().getStat().getNumChildren()).isEqualTo(0); 66 | 67 | long initialPzxid = parent.unwrapOrElseThrow().getStat().getPzxid(); 68 | 69 | Result result = fdb.run( 70 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 71 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 72 | 73 | parent = fdb.run(tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest("/", false))).join(); 74 | assertThat(parent.unwrapOrElseThrow().getStat().getPzxid()).isGreaterThan(initialPzxid); 75 | assertThat(parent.unwrapOrElseThrow().getStat().getCversion()).isEqualTo(1); 76 | assertThat(parent.unwrapOrElseThrow().getStat().getNumChildren()).isEqualTo(1); 77 | } 78 | 79 | @Test 80 | public void itCreatesSequentialNodes() { 81 | Result result = fdb.run( 82 | tr -> fdbCreateOp.execute( 83 | REQUEST, tr, 84 | new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), CreateMode.PERSISTENT_SEQUENTIAL.toFlag())).join()); 85 | 86 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH + "0000000000")); 87 | 88 | result = fdb.run( 89 | tr -> fdbCreateOp.execute( 90 | REQUEST, tr, 91 | new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), CreateMode.PERSISTENT_SEQUENTIAL.toFlag())).join()); 92 | 93 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH + "0000000001")); 94 | 95 | Result exists = fdb.run( 96 | tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH, false))).join(); 97 | assertThat(exists.isOk()).isFalse(); 98 | 99 | exists = fdb.run( 100 | tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH + "0000000002", false))).join(); 101 | assertThat(exists.isOk()).isFalse(); 102 | 103 | exists = fdb.run( 104 | tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH + "0000000001", false))).join(); 105 | assertThat(exists.isOk()).isTrue(); 106 | } 107 | 108 | @Test 109 | public void itRecordsEphemeralNodes() { 110 | Result result = fdb.run( 111 | tr -> fdbCreateOp.execute( 112 | REQUEST, tr, 113 | new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), CreateMode.EPHEMERAL.toFlag())).join()); 114 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 115 | 116 | result = fdb.run( 117 | tr -> fdbCreateOp.execute( 118 | REQUEST, tr, 119 | new CreateRequest("/a-persistent-node", new byte[0], Collections.emptyList(), CreateMode.PERSISTENT.toFlag())).join()); 120 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse("/a-persistent-node")); 121 | 122 | result = fdb.run( 123 | tr -> fdbCreateOp.execute( 124 | REQUEST, tr, 125 | new CreateRequest("/a-persistent-node/something-else", new byte[0], Collections.emptyList(), CreateMode.EPHEMERAL.toFlag())).join()); 126 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse("/a-persistent-node/something-else")); 127 | 128 | Iterable ephemeralNodePaths = fdb.run(tr -> fdbEphemeralNodeManager.getEphemeralNodeZkPaths(tr, REQUEST.sessionId)).join(); 129 | assertThat(ephemeralNodePaths).containsExactlyInAnyOrder(BASE_PATH, "/a-persistent-node/something-else"); 130 | } 131 | 132 | @Test 133 | public void itFailsToCreateChildNodeOfEphemeral() { 134 | Result result = fdb.run( 135 | tr -> fdbCreateOp.execute( 136 | REQUEST, tr, 137 | new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), CreateMode.EPHEMERAL.toFlag())).join()); 138 | 139 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 140 | 141 | result = fdb.run( 142 | tr -> fdbCreateOp.execute( 143 | REQUEST, tr, 144 | new CreateRequest(BASE_PATH + "/abc", new byte[0], Collections.emptyList(), CreateMode.PERSISTENT_SEQUENTIAL.toFlag())).join()); 145 | 146 | assertThat(result.unwrapErrOrElseThrow().code()).isEqualTo(Code.NOCHILDRENFOREPHEMERALS); 147 | } 148 | 149 | @Test 150 | public void itTriggersWatchForNodeCreation() throws InterruptedException { 151 | CountDownLatch countDownLatch = new CountDownLatch(1); 152 | Watcher watcher = event -> { 153 | assertThat(event.getType()).isEqualTo(EventType.NodeCreated); 154 | assertThat(event.getPath()).isEqualTo(BASE_PATH); 155 | countDownLatch.countDown(); 156 | }; 157 | 158 | fdb.run(tr -> { 159 | fdbWatchManager.addNodeCreatedWatch(tr, BASE_PATH, watcher, REQUEST.sessionId); 160 | return null; 161 | }); 162 | 163 | Result result = fdb.run( 164 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 165 | 166 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 167 | assertThat(SERVER_CNXN.getWatchedEvents().peek()).isNull(); 168 | 169 | assertThat(countDownLatch.await(2, TimeUnit.SECONDS)).isTrue(); 170 | } 171 | 172 | @Test 173 | public void itTriggersWatchesOnParentNode() throws InterruptedException { 174 | CountDownLatch countDownLatch = new CountDownLatch(1); 175 | 176 | Watcher watcher = event -> { 177 | assertThat(event.getType()).isEqualTo(EventType.NodeChildrenChanged); 178 | assertThat(event.getPath()).isEqualTo("/"); 179 | countDownLatch.countDown(); 180 | }; 181 | 182 | fdb.run(tr -> { 183 | fdbWatchManager.addNodeChildrenWatch(tr, "/", watcher, REQUEST.sessionId); 184 | return null; 185 | }); 186 | 187 | Result result = fdb.run( 188 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 189 | 190 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 191 | assertThat(SERVER_CNXN.getWatchedEvents().peek()).isNull(); 192 | 193 | assertThat(countDownLatch.await(2, TimeUnit.SECONDS)).isTrue(); 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/ops/FdbDeleteOpTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import java.util.Collections; 7 | import java.util.concurrent.CountDownLatch; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | import org.apache.zookeeper.CreateMode; 11 | import org.apache.zookeeper.KeeperException; 12 | import org.apache.zookeeper.KeeperException.Code; 13 | import org.apache.zookeeper.OpResult.DeleteResult; 14 | import org.apache.zookeeper.Watcher; 15 | import org.apache.zookeeper.Watcher.Event.EventType; 16 | import org.apache.zookeeper.proto.CreateRequest; 17 | import org.apache.zookeeper.proto.CreateResponse; 18 | import org.apache.zookeeper.proto.DeleteRequest; 19 | import org.apache.zookeeper.proto.ExistsRequest; 20 | import org.apache.zookeeper.proto.ExistsResponse; 21 | import org.apache.zookeeper.proto.GetChildrenRequest; 22 | import org.apache.zookeeper.proto.GetChildrenResponse; 23 | import org.apache.zookeeper.proto.SetDataRequest; 24 | import org.junit.Test; 25 | 26 | import com.hubspot.algebra.Result; 27 | import com.ph14.fdb.zk.FdbBaseTest; 28 | import com.ph14.fdb.zk.layer.changefeed.WatchEventChangefeed; 29 | 30 | public class FdbDeleteOpTest extends FdbBaseTest { 31 | 32 | @Test 33 | public void itReturnsErrorIfNodeDoesntExist() { 34 | Result result = fdb.run( 35 | tr -> fdbDeleteOp.execute(REQUEST, tr, new DeleteRequest(BASE_PATH, 0))).join(); 36 | assertThat(result.unwrapErrOrElseThrow().code()).isEqualTo(Code.NONODE); 37 | } 38 | 39 | @Test 40 | public void itReturnsErrorIfVersionDoesntMatch() { 41 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 42 | fdb.run(tr -> fdbSetDataOp.execute(REQUEST, tr, new SetDataRequest(BASE_PATH, "a".getBytes(), 1))).join(); 43 | fdb.run(tr -> fdbSetDataOp.execute(REQUEST, tr, new SetDataRequest(BASE_PATH, "b".getBytes(), 2))).join(); 44 | 45 | Result result = fdb.run( 46 | tr -> fdbDeleteOp.execute(REQUEST, tr, new DeleteRequest(BASE_PATH, 2))).join(); 47 | 48 | assertThat(result.unwrapErrOrElseThrow().code()).isEqualTo(Code.BADVERSION); 49 | } 50 | 51 | @Test 52 | public void itReturnsErrorIfNodeHasChildren() { 53 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 54 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(SUBPATH, new byte[0], Collections.emptyList(), 0))).join(); 55 | 56 | Result result = fdb.run( 57 | tr -> fdbDeleteOp.execute(REQUEST, tr, new DeleteRequest(BASE_PATH, 1))).join(); 58 | 59 | assertThat(result.unwrapErrOrElseThrow().code()).isEqualTo(Code.NOTEMPTY); 60 | } 61 | 62 | @Test 63 | public void itDeletesIfVersionMatchesExactly() { 64 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 65 | fdb.run(tr -> fdbSetDataOp.execute(REQUEST, tr, new SetDataRequest(BASE_PATH, "a".getBytes(), 0))).join(); 66 | fdb.run(tr -> fdbSetDataOp.execute(REQUEST, tr, new SetDataRequest(BASE_PATH, "b".getBytes(), 1))).join(); 67 | 68 | Result result = fdb.run( 69 | tr -> fdbDeleteOp.execute(REQUEST, tr, new DeleteRequest(BASE_PATH, 2))).join(); 70 | 71 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new DeleteResult()); 72 | } 73 | 74 | @Test 75 | public void itDeletesIfVersionIsAllVersionsFlag() { 76 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 77 | 78 | Result result = fdb.run( 79 | tr -> fdbDeleteOp.execute(REQUEST, tr, new DeleteRequest(BASE_PATH, -1))).join(); 80 | 81 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new DeleteResult()); 82 | } 83 | 84 | @Test 85 | public void itUpdatesParentStatAfterSuccessfulDeletion() { 86 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 87 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(SUBPATH, new byte[0], Collections.emptyList(), 0))).join(); 88 | 89 | Result exists = fdb.run( 90 | tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH, false))).join(); 91 | assertThat(exists.unwrapOrElseThrow().getStat().getCversion()).isEqualTo(1); 92 | assertThat(exists.unwrapOrElseThrow().getStat().getNumChildren()).isEqualTo(1); 93 | long initialPzxid = exists.unwrapOrElseThrow().getStat().getPzxid(); 94 | 95 | fdb.run(tr -> fdbDeleteOp.execute(REQUEST, tr, new DeleteRequest(SUBPATH, 0))).join(); 96 | 97 | exists = fdb.run(tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH, false))).join(); 98 | assertThat(exists.unwrapOrElseThrow().getStat().getCversion()).isEqualTo(2); 99 | assertThat(exists.unwrapOrElseThrow().getStat().getNumChildren()).isEqualTo(0); 100 | assertThat(exists.unwrapOrElseThrow().getStat().getPzxid()).isGreaterThan(initialPzxid); 101 | } 102 | 103 | @Test 104 | public void itDoesntPerformWritesIfExceptionIsThrown() { 105 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 106 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(SUBPATH, new byte[0], Collections.emptyList(), 0))).join(); 107 | 108 | Result exists = fdb.run( 109 | tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH, false))).join(); 110 | assertThat(exists.unwrapOrElseThrow().getStat().getCversion()).isEqualTo(1); 111 | assertThat(exists.unwrapOrElseThrow().getStat().getNumChildren()).isEqualTo(1); 112 | long initialPzxid = exists.unwrapOrElseThrow().getStat().getPzxid(); 113 | 114 | FdbDeleteOp throwingFdbDeleteOp = new FdbDeleteOp(fdbNodeReader, fdbNodeWriter, new ThrowingWatchManager(new WatchEventChangefeed(fdb)), fdbEphemeralNodeManager); 115 | assertThatThrownBy(() -> fdb.run(tr -> throwingFdbDeleteOp.execute(REQUEST, tr, new DeleteRequest(SUBPATH, 0)))) 116 | .hasCauseInstanceOf(RuntimeException.class); 117 | 118 | exists = fdb.run(tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH, false))).join(); 119 | assertThat(exists.unwrapOrElseThrow().getStat().getCversion()).isEqualTo(1); 120 | assertThat(exists.unwrapOrElseThrow().getStat().getNumChildren()).isEqualTo(1); 121 | assertThat(exists.unwrapOrElseThrow().getStat().getPzxid()).isEqualTo(initialPzxid); 122 | } 123 | 124 | @Test 125 | public void itRemovesEphemeralNodesFromManager() { 126 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), CreateMode.PERSISTENT.toFlag()))).join(); 127 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(SUBPATH, new byte[0], Collections.emptyList(), CreateMode.EPHEMERAL.toFlag()))).join(); 128 | 129 | Result children = fdb.run(tr -> fdbGetChildrenOp.execute(REQUEST, tr, new GetChildrenRequest(BASE_PATH, false)).join()); 130 | assertThat(children.isOk()).isTrue(); 131 | assertThat(children.unwrapOrElseThrow().getChildren()).containsExactly("bar"); 132 | 133 | fdb.run(tr -> fdbDeleteOp.execute(REQUEST, tr, new DeleteRequest(SUBPATH, -1))).join(); 134 | 135 | children = fdb.run(tr -> fdbGetChildrenOp.execute(REQUEST, tr, new GetChildrenRequest(BASE_PATH, false)).join()); 136 | assertThat(children.isOk()).isTrue(); 137 | assertThat(children.unwrapOrElseThrow().getChildren()).isEmpty(); 138 | 139 | Iterable ephemeralNodePaths = fdb.run(tr -> fdbEphemeralNodeManager.getEphemeralNodeZkPaths(tr, REQUEST.sessionId)).join(); 140 | assertThat(ephemeralNodePaths).isEmpty(); 141 | } 142 | 143 | @Test 144 | public void itTriggersWatchForNodeDeletion() throws InterruptedException { 145 | CountDownLatch countDownLatch = new CountDownLatch(1); 146 | Watcher watcher = event -> { 147 | assertThat(event.getType()).isEqualTo(EventType.NodeDeleted); 148 | assertThat(event.getPath()).isEqualTo(BASE_PATH); 149 | countDownLatch.countDown(); 150 | }; 151 | 152 | fdb.run(tr -> { 153 | fdbWatchManager.addNodeDeletedWatch(tr, BASE_PATH, watcher, REQUEST.sessionId); 154 | return null; 155 | }); 156 | 157 | Result result = fdb.run( 158 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 159 | 160 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 161 | assertThat(SERVER_CNXN.getWatchedEvents().peek()).isNull(); 162 | 163 | fdb.run(tr -> fdbDeleteOp.execute(REQUEST, tr, new DeleteRequest(BASE_PATH, -1))).join(); 164 | assertThat(countDownLatch.await(2, TimeUnit.SECONDS)).isTrue(); 165 | } 166 | 167 | @Test 168 | public void itTriggersWatchesOnParentNodeDeletion() throws InterruptedException { 169 | CountDownLatch countDownLatch = new CountDownLatch(1); 170 | 171 | Watcher watcher = event -> { 172 | assertThat(event.getType()).isEqualTo(EventType.NodeChildrenChanged); 173 | assertThat(event.getPath()).isEqualTo("/"); 174 | countDownLatch.countDown(); 175 | }; 176 | 177 | Result result = fdb.run( 178 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, new byte[0], Collections.emptyList(), 0))).join(); 179 | 180 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 181 | assertThat(SERVER_CNXN.getWatchedEvents().peek()).isNull(); 182 | 183 | fdb.run(tr -> { 184 | fdbWatchManager.addNodeChildrenWatch(tr, "/", watcher, REQUEST.sessionId); 185 | return null; 186 | }); 187 | 188 | fdb.run(tr -> fdbDeleteOp.execute(REQUEST, tr, new DeleteRequest(BASE_PATH, -1))).join(); 189 | assertThat(countDownLatch.await(2, TimeUnit.SECONDS)).isTrue(); 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/ops/FdbExistsOpTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.Collections; 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import org.apache.zookeeper.KeeperException; 10 | import org.apache.zookeeper.KeeperException.Code; 11 | import org.apache.zookeeper.WatchedEvent; 12 | import org.apache.zookeeper.Watcher.Event.EventType; 13 | import org.apache.zookeeper.data.Stat; 14 | import org.apache.zookeeper.proto.CreateRequest; 15 | import org.apache.zookeeper.proto.CreateResponse; 16 | import org.apache.zookeeper.proto.ExistsRequest; 17 | import org.apache.zookeeper.proto.ExistsResponse; 18 | import org.junit.Test; 19 | 20 | import com.hubspot.algebra.Result; 21 | import com.ph14.fdb.zk.FdbBaseTest; 22 | 23 | public class FdbExistsOpTest extends FdbBaseTest { 24 | 25 | @Test 26 | public void itFindsStatOfExistingNode() { 27 | byte[] data = "some string thing".getBytes(); 28 | long timeBeforeExecution = System.currentTimeMillis(); 29 | 30 | Result result = fdb.run( 31 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, data, Collections.emptyList(), 0))).join(); 32 | 33 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 34 | 35 | Result exists = fdb.run( 36 | tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH, false))).join(); 37 | 38 | assertThat(exists.unwrapOrElseThrow().getStat()).isNotNull(); 39 | 40 | Stat stat = exists.unwrapOrElseThrow().getStat(); 41 | 42 | assertThat(stat.getCzxid()).isGreaterThan(0L); 43 | assertThat(stat.getMzxid()).isGreaterThan(0L); 44 | assertThat(stat.getCtime()).isGreaterThanOrEqualTo(timeBeforeExecution); 45 | assertThat(stat.getMtime()).isGreaterThanOrEqualTo(timeBeforeExecution); 46 | assertThat(stat.getVersion()).isEqualTo(0); 47 | assertThat(stat.getCversion()).isEqualTo(0); 48 | assertThat(stat.getDataLength()).isEqualTo(data.length); 49 | } 50 | 51 | @Test 52 | public void itReturnsErrorIfNodeDoesNotExist() { 53 | Result exists = fdbExistsOp.execute(REQUEST, transaction, new ExistsRequest(BASE_PATH, false)).join(); 54 | assertThat(exists.unwrapErrOrElseThrow().code()).isEqualTo(Code.NONODE); 55 | } 56 | 57 | @Test 58 | public void itSetsWatchForDataUpdateIfNodeExists() throws InterruptedException { 59 | Result result = fdb.run( 60 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, "hello".getBytes(), Collections.emptyList(), 0))).join(); 61 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 62 | 63 | Result result2 = fdb.run( 64 | tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH, true))).join(); 65 | 66 | fdb.run(tr -> { 67 | fdbWatchManager.triggerNodeUpdatedWatch(tr, BASE_PATH); 68 | return null; 69 | }); 70 | 71 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 72 | assertThat(event).isNotNull(); 73 | assertThat(event.getType()).isEqualTo(EventType.NodeDataChanged); 74 | assertThat(event.getPath()).isEqualTo(BASE_PATH); 75 | } 76 | 77 | @Test 78 | public void itSetsWatchForNodeCreationIfNodeDoesNotExist() throws InterruptedException { 79 | Result result2 = fdb.run( 80 | tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH, true))).join(); 81 | assertThat(result2.isOk()).isFalse(); 82 | 83 | fdb.run(tr -> { 84 | fdbWatchManager.triggerNodeCreatedWatch(tr, BASE_PATH); 85 | return null; 86 | }); 87 | 88 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 89 | assertThat(event).isNotNull(); 90 | assertThat(event.getType()).isEqualTo(EventType.NodeCreated); 91 | assertThat(event.getPath()).isEqualTo(BASE_PATH); 92 | } 93 | 94 | @Test 95 | public void itSetsWatchForNodeDeletionIfNodeExists() throws InterruptedException { 96 | Result result = fdb.run( 97 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, "hello".getBytes(), Collections.emptyList(), 0))).join(); 98 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 99 | 100 | Result result2 = fdb.run( 101 | tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH, true))).join(); 102 | assertThat(result2.isOk()).isTrue(); 103 | 104 | fdb.run(tr -> { 105 | fdbWatchManager.triggerNodeDeletedWatch(tr, BASE_PATH); 106 | return null; 107 | }); 108 | 109 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 110 | assertThat(event).isNotNull(); 111 | assertThat(event.getType()).isEqualTo(EventType.NodeDeleted); 112 | assertThat(event.getPath()).isEqualTo(BASE_PATH); 113 | } 114 | 115 | @Test 116 | public void itPlaysPendingWatchesBeforeReturning() throws InterruptedException { 117 | fdb.run(tr -> { 118 | CompletableFuture fdbWatch = watchEventChangefeed.setZKChangefeedWatch(tr, SERVER_CNXN, REQUEST.sessionId, EventType.NodeCreated, "abc"); 119 | fdbWatch.cancel(true); 120 | watchEventChangefeed.appendToChangefeed(tr, EventType.NodeCreated, "abc").join(); 121 | return null; 122 | }); 123 | 124 | Result result2 = fdb.run( 125 | tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH, true))).join(); 126 | assertThat(result2.isOk()).isFalse(); 127 | 128 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 129 | assertThat(event).isNotNull(); 130 | assertThat(event.getType()).isEqualTo(EventType.NodeCreated); 131 | assertThat(event.getPath()).isEqualTo("abc"); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/ops/FdbGetChildrenWithStatOpTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.Collections; 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import org.apache.zookeeper.KeeperException; 10 | import org.apache.zookeeper.KeeperException.Code; 11 | import org.apache.zookeeper.WatchedEvent; 12 | import org.apache.zookeeper.Watcher.Event.EventType; 13 | import org.apache.zookeeper.proto.CreateRequest; 14 | import org.apache.zookeeper.proto.GetChildren2Request; 15 | import org.apache.zookeeper.proto.GetChildren2Response; 16 | import org.junit.Test; 17 | 18 | import com.hubspot.algebra.Result; 19 | import com.ph14.fdb.zk.FdbBaseTest; 20 | 21 | public class FdbGetChildrenWithStatOpTest extends FdbBaseTest { 22 | 23 | @Test 24 | public void itListsNoChildrenWhenThereAreNoChildren() { 25 | Result result = fdb.run( 26 | tr -> fdbGetChildrenWithStatOp.execute(REQUEST, tr, new GetChildren2Request("/", false))).join(); 27 | assertThat(result.unwrapOrElseThrow().getChildren()).isEmpty(); 28 | } 29 | 30 | @Test 31 | public void itReturnsErrorIfNodeDoesntExist() { 32 | Result result = fdb.run( 33 | tr -> fdbGetChildrenWithStatOp.execute(REQUEST, tr, new GetChildren2Request(BASE_PATH, false))).join(); 34 | assertThat(result.unwrapErrOrElseThrow().code()).isEqualTo(Code.NONODE); 35 | } 36 | 37 | @Test 38 | public void itReturnsChildrenOfRoot() { 39 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest("/pwh", new byte[0], Collections.emptyList(), 0))).join(); 40 | 41 | Result result = fdb.run( 42 | tr -> fdbGetChildrenWithStatOp.execute(REQUEST, tr, new GetChildren2Request("/", false))).join(); 43 | 44 | assertThat(result.unwrapOrElseThrow().getChildren()).containsExactly("pwh"); 45 | assertThat(result.unwrapOrElseThrow().getStat().getNumChildren()).isEqualTo(1); 46 | } 47 | 48 | @Test 49 | public void itReturnsChildren() { 50 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest("/abc", new byte[0], Collections.emptyList(), 0))).join(); 51 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest("/abc/def", new byte[0], Collections.emptyList(), 0))).join(); 52 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest("/abc/ghi", new byte[0], Collections.emptyList(), 0))).join(); 53 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest("/abc/ghi/xyz", new byte[0], Collections.emptyList(), 0))).join(); 54 | 55 | Result result = fdb.run( 56 | tr -> fdbGetChildrenWithStatOp.execute(REQUEST, tr, new GetChildren2Request("/", false))).join(); 57 | assertThat(result.unwrapOrElseThrow().getChildren()).containsExactly("abc"); 58 | assertThat(result.unwrapOrElseThrow().getStat().getNumChildren()).isEqualTo(1); 59 | 60 | result = fdb.run(tr -> fdbGetChildrenWithStatOp.execute(REQUEST, tr, new GetChildren2Request("/abc", false))).join(); 61 | assertThat(result.unwrapOrElseThrow().getChildren()).containsExactly("def", "ghi"); 62 | assertThat(result.unwrapOrElseThrow().getStat().getNumChildren()).isEqualTo(2); 63 | 64 | result = fdb.run(tr -> fdbGetChildrenWithStatOp.execute(REQUEST, tr, new GetChildren2Request("/abc/def", false))).join(); 65 | assertThat(result.unwrapOrElseThrow().getChildren()).isEmpty(); 66 | assertThat(result.unwrapOrElseThrow().getStat().getNumChildren()).isEqualTo(0); 67 | 68 | result = fdb.run(tr -> fdbGetChildrenWithStatOp.execute(REQUEST, tr, new GetChildren2Request("/abc/ghi", false))).join(); 69 | assertThat(result.unwrapOrElseThrow().getChildren()).containsExactly("xyz"); 70 | assertThat(result.unwrapOrElseThrow().getStat().getNumChildren()).isEqualTo(1); 71 | } 72 | 73 | @Test 74 | public void itSetsWatchForParentDeletion() throws InterruptedException { 75 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest("/abc", "hello".getBytes(), Collections.emptyList(), 0))).join(); 76 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest("/abc/def", "hello".getBytes(), Collections.emptyList(), 0))).join(); 77 | 78 | fdb.run(tr -> fdbGetChildrenWithStatOp.execute(REQUEST, tr, new GetChildren2Request("/abc", true))).join(); 79 | 80 | fdb.run(tr -> { 81 | fdbWatchManager.triggerNodeDeletedWatch(tr, "/abc"); 82 | return null; 83 | }); 84 | 85 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 86 | assertThat(event).isNotNull(); 87 | assertThat(event.getType()).isEqualTo(EventType.NodeDeleted); 88 | assertThat(event.getPath()).isEqualTo("/abc"); 89 | } 90 | 91 | @Test 92 | public void itSetsChildWatch() throws InterruptedException { 93 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest("/abc", "hello".getBytes(), Collections.emptyList(), 0))).join(); 94 | fdb.run(tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest("/abc/def", "hello".getBytes(), Collections.emptyList(), 0))).join(); 95 | 96 | fdb.run(tr -> fdbGetChildrenWithStatOp.execute(REQUEST, tr, new GetChildren2Request("/abc", true))).join(); 97 | 98 | fdb.run(tr -> { 99 | fdbWatchManager.triggerNodeChildrenWatch(tr, "/abc"); 100 | return null; 101 | }); 102 | 103 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 104 | assertThat(event).isNotNull(); 105 | assertThat(event.getType()).isEqualTo(EventType.NodeChildrenChanged); 106 | assertThat(event.getPath()).isEqualTo("/abc"); 107 | } 108 | 109 | @Test 110 | public void itPlaysPendingWatchesBeforeReturning() throws InterruptedException { 111 | fdb.run(tr -> { 112 | CompletableFuture fdbWatch = watchEventChangefeed.setZKChangefeedWatch(tr, SERVER_CNXN, REQUEST.sessionId, EventType.NodeCreated, "abc"); 113 | fdbWatch.cancel(true); 114 | watchEventChangefeed.appendToChangefeed(tr, EventType.NodeCreated, "abc").join(); 115 | return null; 116 | }); 117 | 118 | Result result2 = fdb.run( 119 | tr -> fdbGetChildrenWithStatOp.execute(REQUEST, tr, new GetChildren2Request(BASE_PATH, true))).join(); 120 | assertThat(result2.isOk()).isFalse(); 121 | 122 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 123 | assertThat(event).isNotNull(); 124 | assertThat(event.getType()).isEqualTo(EventType.NodeCreated); 125 | assertThat(event.getPath()).isEqualTo("abc"); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/ops/FdbGetDataOpTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.Collections; 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import org.apache.zookeeper.KeeperException; 10 | import org.apache.zookeeper.KeeperException.Code; 11 | import org.apache.zookeeper.WatchedEvent; 12 | import org.apache.zookeeper.Watcher.Event.EventType; 13 | import org.apache.zookeeper.proto.CreateRequest; 14 | import org.apache.zookeeper.proto.CreateResponse; 15 | import org.apache.zookeeper.proto.GetDataRequest; 16 | import org.apache.zookeeper.proto.GetDataResponse; 17 | import org.junit.Test; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import com.google.common.base.Strings; 22 | import com.hubspot.algebra.Result; 23 | import com.ph14.fdb.zk.FdbBaseTest; 24 | 25 | public class FdbGetDataOpTest extends FdbBaseTest { 26 | 27 | private static final Logger LOG = LoggerFactory.getLogger(FdbGetDataOpTest.class); 28 | 29 | @Test 30 | public void itGetsDataFromExistingNode() { 31 | long timeBeforeCreation = System.currentTimeMillis(); 32 | String data = Strings.repeat("this is the song that never ends ", 10000); 33 | 34 | Result result = fdb.run( 35 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, data.getBytes(), Collections.emptyList(), 0))).join(); 36 | 37 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 38 | 39 | Result result2 = fdb.run(tr -> fdbGetDataOp.execute(REQUEST, tr, new GetDataRequest(BASE_PATH, false))).join(); 40 | 41 | GetDataResponse getDataResponse = result2.unwrapOrElseThrow(); 42 | 43 | assertThat(getDataResponse.getData()).isEqualTo(data.getBytes()); 44 | assertThat(getDataResponse.getStat().getMzxid()).isGreaterThan(0L); 45 | assertThat(getDataResponse.getStat().getMzxid()).isEqualTo(getDataResponse.getStat().getCzxid()); 46 | assertThat(getDataResponse.getStat().getVersion()).isEqualTo(0); 47 | assertThat(getDataResponse.getStat().getCtime()).isGreaterThanOrEqualTo(timeBeforeCreation); 48 | assertThat(getDataResponse.getStat().getCtime()).isEqualTo(getDataResponse.getStat().getMtime()); 49 | assertThat(getDataResponse.getStat().getAversion()).isEqualTo(0); 50 | assertThat(getDataResponse.getStat().getNumChildren()).isEqualTo(0); 51 | assertThat(getDataResponse.getStat().getEphemeralOwner()).isEqualTo(0); 52 | assertThat(getDataResponse.getStat().getDataLength()).isEqualTo(data.getBytes().length); 53 | } 54 | 55 | @Test 56 | public void itReturnsErrorIfNodeDoesNotExist() { 57 | Result exists = fdbGetDataOp.execute(REQUEST, transaction, new GetDataRequest(BASE_PATH, false)).join(); 58 | assertThat(exists.unwrapErrOrElseThrow().code()).isEqualTo(Code.NONODE); 59 | } 60 | 61 | @Test 62 | public void itSetsWatchForDataUpdate() throws InterruptedException { 63 | Result result = fdb.run( 64 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, "hello".getBytes(), Collections.emptyList(), 0))).join(); 65 | 66 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 67 | 68 | Result result2 = fdb.run(tr -> fdbGetDataOp.execute(REQUEST, tr, new GetDataRequest(BASE_PATH, true))).join(); 69 | assertThat(result2.isOk()).isTrue(); 70 | 71 | fdb.run(tr -> { 72 | fdbWatchManager.triggerNodeUpdatedWatch(tr, BASE_PATH); 73 | return null; 74 | }); 75 | 76 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 77 | assertThat(event).isNotNull(); 78 | assertThat(event.getType()).isEqualTo(EventType.NodeDataChanged); 79 | assertThat(event.getPath()).isEqualTo(BASE_PATH); 80 | } 81 | 82 | @Test 83 | public void itSetsWatchForNodeDeletion() throws InterruptedException { 84 | Result result = fdb.run( 85 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, "hello".getBytes(), Collections.emptyList(), 0))).join(); 86 | 87 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 88 | 89 | Result result2 = fdb.run(tr -> fdbGetDataOp.execute(REQUEST, tr, new GetDataRequest(BASE_PATH, true))).join(); 90 | assertThat(result2.isOk()).isTrue(); 91 | 92 | fdb.run(tr -> { 93 | fdbWatchManager.triggerNodeDeletedWatch(tr, BASE_PATH); 94 | return null; 95 | }); 96 | 97 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 98 | assertThat(event).isNotNull(); 99 | assertThat(event.getType()).isEqualTo(EventType.NodeDeleted); 100 | assertThat(event.getPath()).isEqualTo(BASE_PATH); 101 | } 102 | 103 | @Test 104 | public void itDoesntTriggerTwoWatchesForUpdateAndDelete() throws InterruptedException { 105 | Result result = fdb.run( 106 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, "hello".getBytes(), Collections.emptyList(), 0))).join(); 107 | 108 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 109 | 110 | Result result2 = fdb.run(tr -> fdbGetDataOp.execute(REQUEST, tr, new GetDataRequest(BASE_PATH, true))).join(); 111 | assertThat(result2.isOk()).isTrue(); 112 | 113 | fdb.run(tr -> { 114 | fdbWatchManager.triggerNodeUpdatedWatch(tr, BASE_PATH); 115 | fdbWatchManager.triggerNodeDeletedWatch(tr, BASE_PATH); 116 | return null; 117 | }); 118 | 119 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 120 | assertThat(event).isNotNull(); 121 | 122 | event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 123 | assertThat(event).isNull(); 124 | } 125 | 126 | @Test 127 | public void itPlaysPendingWatchesBeforeReturning() throws InterruptedException { 128 | fdb.run(tr -> { 129 | CompletableFuture fdbWatch = watchEventChangefeed.setZKChangefeedWatch(tr, SERVER_CNXN, REQUEST.sessionId, EventType.NodeCreated, "abc"); 130 | fdbWatch.cancel(true); 131 | watchEventChangefeed.appendToChangefeed(tr, EventType.NodeCreated, "abc").join(); 132 | return null; 133 | }); 134 | 135 | Result result2 = fdb.run( 136 | tr -> fdbGetDataOp.execute(REQUEST, tr, new GetDataRequest(BASE_PATH, true))).join(); 137 | assertThat(result2.isOk()).isFalse(); 138 | 139 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 140 | assertThat(event).isNotNull(); 141 | assertThat(event.getType()).isEqualTo(EventType.NodeCreated); 142 | assertThat(event.getPath()).isEqualTo("abc"); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/ops/FdbMultiOpTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | import org.apache.zookeeper.KeeperException; 10 | import org.apache.zookeeper.KeeperException.Code; 11 | import org.apache.zookeeper.MultiResponse; 12 | import org.apache.zookeeper.MultiTransactionRecord; 13 | import org.apache.zookeeper.Op; 14 | import org.apache.zookeeper.OpResult.CreateResult; 15 | import org.apache.zookeeper.OpResult.ErrorResult; 16 | import org.apache.zookeeper.proto.ExistsRequest; 17 | import org.apache.zookeeper.proto.ExistsResponse; 18 | import org.junit.Ignore; 19 | import org.junit.Test; 20 | 21 | import com.google.common.base.Strings; 22 | import com.hubspot.algebra.Result; 23 | import com.ph14.fdb.zk.FdbBaseTest; 24 | 25 | public class FdbMultiOpTest extends FdbBaseTest { 26 | 27 | static final String data = "data"; 28 | 29 | @Test 30 | @Ignore 31 | public void itCreatesManyNodesAtOnce() { 32 | MultiTransactionRecord ops = new MultiTransactionRecord(); 33 | ops.add(Op.create("/bac", data.getBytes(), Collections.emptyList(), 0)); 34 | ops.add(Op.create("/abc", data.getBytes(), Collections.emptyList(), 0)); 35 | ops.add(Op.create("/cba", data.getBytes(), Collections.emptyList(), 0)); 36 | 37 | MultiResponse opResults = fdbMultiOp.execute(REQUEST, ops); 38 | assertThat(opResults.getResultList()) 39 | .containsExactly( 40 | new CreateResult("/bac"), 41 | new CreateResult("/abc"), 42 | new CreateResult("/cba")); 43 | 44 | List createVersions = new ArrayList<>(); 45 | fdb.run(tr -> { 46 | createVersions.add(fdbExistsOp.execute(REQUEST, tr, new ExistsRequest("/bac", false)).join() 47 | .unwrapOrElseThrow().getStat().getCzxid()); 48 | createVersions.add(fdbExistsOp.execute(REQUEST, tr, new ExistsRequest("/abc", false)).join() 49 | .unwrapOrElseThrow().getStat().getCzxid()); 50 | createVersions.add(fdbExistsOp.execute(REQUEST, tr, new ExistsRequest("/cba", false)).join() 51 | .unwrapOrElseThrow().getStat().getCzxid()); 52 | return createVersions; 53 | }); 54 | 55 | assertThat(createVersions).hasSize(3); 56 | assertThat(createVersions.get(0)).isEqualTo(createVersions.get(1)); 57 | assertThat(createVersions.get(0)).isEqualTo(createVersions.get(2)); 58 | } 59 | 60 | @Test 61 | public void itDoesntSetIfOneOpFails() { 62 | final String data = Strings.repeat("this is the song that never ends ", 10000); 63 | 64 | MultiTransactionRecord ops = new MultiTransactionRecord(); 65 | ops.add(Op.create(BASE_PATH, data.getBytes(), Collections.emptyList(), 0)); 66 | ops.add(Op.check(SUBPATH, 15)); 67 | 68 | MultiResponse opResults = fdbMultiOp.execute(REQUEST, ops); 69 | assertThat(opResults.getResultList()) 70 | .containsExactly(new CreateResult(BASE_PATH), new ErrorResult(Code.NONODE.intValue())); 71 | 72 | Result result = fdb.run( 73 | tr -> fdbExistsOp.execute(REQUEST, tr, new ExistsRequest(BASE_PATH, false))).join(); 74 | 75 | assertThat(result.unwrapErrOrElseThrow().code().intValue()).isEqualTo(Code.NONODE.intValue()); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/ops/FdbSetDataOpTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import java.io.IOException; 7 | import java.util.Collections; 8 | import java.util.concurrent.CountDownLatch; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import org.apache.zookeeper.KeeperException; 12 | import org.apache.zookeeper.KeeperException.Code; 13 | import org.apache.zookeeper.Watcher; 14 | import org.apache.zookeeper.Watcher.Event.EventType; 15 | import org.apache.zookeeper.proto.CreateRequest; 16 | import org.apache.zookeeper.proto.CreateResponse; 17 | import org.apache.zookeeper.proto.SetDataRequest; 18 | import org.apache.zookeeper.proto.SetDataResponse; 19 | import org.junit.Test; 20 | 21 | import com.google.common.base.Strings; 22 | import com.hubspot.algebra.Result; 23 | import com.ph14.fdb.zk.FdbBaseTest; 24 | 25 | public class FdbSetDataOpTest extends FdbBaseTest { 26 | 27 | @Test 28 | public void itSetsDataForExistingNode() { 29 | final String data = Strings.repeat("this is the song that never ends ", 10000); 30 | 31 | Result result = fdb.run( 32 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, data.getBytes(), Collections.emptyList(), 0))).join(); 33 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 34 | 35 | long now = System.currentTimeMillis(); 36 | 37 | final String data2 = "this is something else"; 38 | Result result2 = fdb.run( 39 | tr -> fdbSetDataOp.execute(REQUEST, tr, new SetDataRequest(BASE_PATH, data2.getBytes(), 0))).join(); 40 | 41 | SetDataResponse setDataResponse = result2.unwrapOrElseThrow(); 42 | assertThat(setDataResponse.getStat().getMtime()).isGreaterThanOrEqualTo(now); 43 | assertThat(setDataResponse.getStat().getCtime()).isLessThan(setDataResponse.getStat().getMtime()); 44 | assertThat(setDataResponse.getStat().getVersion()).isEqualTo(1); 45 | assertThat(setDataResponse.getStat().getDataLength()).isEqualTo(data2.getBytes().length); 46 | } 47 | 48 | @Test 49 | public void itReturnsErrorIfVersionIsWrong() { 50 | final String data = Strings.repeat("this is the song that never ends ", 10000); 51 | Result result = fdb.run( 52 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, data.getBytes(), Collections.emptyList(), 0))).join(); 53 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 54 | 55 | final String data2 = "this is something else"; 56 | Result result2 = fdb.run( 57 | tr -> fdbSetDataOp.execute(REQUEST, tr, new SetDataRequest(BASE_PATH, data2.getBytes(), 2))).join(); 58 | assertThat(result2.unwrapErrOrElseThrow().code()).isEqualTo(Code.BADVERSION); 59 | } 60 | 61 | @Test 62 | public void itReturnsErrorIfNodeDoesNotExist() { 63 | Result exists = fdbSetDataOp.execute(REQUEST, transaction, new SetDataRequest(BASE_PATH, "hello".getBytes(), 1)).join(); 64 | assertThat(exists.unwrapErrOrElseThrow().code()).isEqualTo(Code.NONODE); 65 | } 66 | 67 | @Test 68 | public void itReturnsErrorIfDataIsTooLarge() { 69 | final String data = Strings.repeat("this is the song that never ends ", 10000); 70 | Result result = fdb.run( 71 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, data.getBytes(), Collections.emptyList(), 0))).join(); 72 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 73 | 74 | final byte[] bigData = new byte[1024 * 1024]; 75 | Result result2 = fdb.run( 76 | tr -> fdbSetDataOp.execute(REQUEST, tr, new SetDataRequest(BASE_PATH, bigData, 0))).join(); 77 | assertThat(result2.isOk()).isTrue(); 78 | 79 | assertThatThrownBy(() -> { 80 | final byte[] webscaleDataTM = new byte[1024 * 1024 + 1]; 81 | fdbSetDataOp.execute(REQUEST, transaction, new SetDataRequest(BASE_PATH, webscaleDataTM, 1)).join(); 82 | }).hasCauseInstanceOf(IOException.class); 83 | } 84 | 85 | @Test 86 | public void itTriggersWatchForDataChange() throws InterruptedException { 87 | Result result = fdb.run(tr -> 88 | fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, "hello".getBytes(), Collections.emptyList(), 0))).join(); 89 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 90 | 91 | CountDownLatch countDownLatch = new CountDownLatch(1); 92 | Watcher watcher = event -> { 93 | assertThat(event.getType()).isEqualTo(EventType.NodeDataChanged); 94 | assertThat(event.getPath()).isEqualTo(BASE_PATH); 95 | countDownLatch.countDown(); 96 | }; 97 | 98 | fdb.run(tr -> { 99 | fdbWatchManager.addNodeDataUpdatedWatch(tr, BASE_PATH, watcher, REQUEST.sessionId); 100 | return null; 101 | }); 102 | 103 | Result exists = fdb.run( 104 | tr -> fdbSetDataOp.execute(REQUEST, tr, new SetDataRequest(BASE_PATH, "hello!".getBytes(), 0))).join(); 105 | 106 | assertThat(exists.isOk()).isTrue(); 107 | assertThat(SERVER_CNXN.getWatchedEvents().peek()).isNull(); 108 | assertThat(countDownLatch.await(1, TimeUnit.SECONDS)).isTrue(); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/ops/ThrowingWatchManager.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.ops; 2 | 3 | import java.util.concurrent.CompletionException; 4 | 5 | import com.apple.foundationdb.Transaction; 6 | import com.ph14.fdb.zk.layer.FdbWatchManager; 7 | import com.ph14.fdb.zk.layer.changefeed.WatchEventChangefeed; 8 | 9 | class ThrowingWatchManager extends FdbWatchManager { 10 | public ThrowingWatchManager(WatchEventChangefeed watchEventChangefeed) { 11 | super(watchEventChangefeed); 12 | } 13 | 14 | @Override 15 | public void triggerNodeCreatedWatch(Transaction transaction, String zkPath) { 16 | throw new CompletionException(new RuntimeException()); 17 | } 18 | 19 | @Override 20 | public void triggerNodeUpdatedWatch(Transaction transaction, String zkPath) { 21 | throw new CompletionException(new RuntimeException()); 22 | } 23 | 24 | @Override 25 | public void triggerNodeDeletedWatch(Transaction transaction, String zkPath) { 26 | throw new CompletionException(new RuntimeException()); 27 | } 28 | 29 | @Override 30 | public void triggerNodeChildrenWatch(Transaction transaction, String zkPath) { 31 | throw new CompletionException(new RuntimeException()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/session/CoordinatingClockTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.session; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.time.Clock; 6 | import java.time.Instant; 7 | import java.time.ZoneId; 8 | import java.util.List; 9 | import java.util.OptionalLong; 10 | import java.util.concurrent.CompletableFuture; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | import java.util.stream.Collectors; 13 | 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | 17 | import com.apple.foundationdb.Database; 18 | import com.apple.foundationdb.FDB; 19 | import com.apple.foundationdb.Range; 20 | import com.apple.foundationdb.tuple.ByteArrayUtil; 21 | import com.apple.foundationdb.tuple.Tuple; 22 | import com.google.common.base.Charsets; 23 | import com.google.common.collect.ImmutableList; 24 | 25 | public class CoordinatingClockTest { 26 | 27 | private Database fdb; 28 | private AtomicBoolean wasLeader; 29 | 30 | @Before 31 | public void setUp() { 32 | this.fdb = FDB.selectAPIVersion(600).open(); 33 | this.wasLeader = new AtomicBoolean(false); 34 | 35 | fdb.run(tr -> { 36 | tr.clear(Range.startsWith(CoordinatingClock.KEY_PREFIX)); 37 | return null; 38 | }); 39 | } 40 | 41 | @Test 42 | public void itStartsAClockWithoutAnInitialKey() { 43 | CoordinatingClock coordinatingClock = CoordinatingClock.from( 44 | fdb, 45 | "session", 46 | () -> wasLeader.set(true), 47 | Clock.fixed(Instant.ofEpochMilli(1050), ZoneId.systemDefault()), 48 | OptionalLong.of(500)); 49 | 50 | assertThat(coordinatingClock.getCurrentTick()).isEmpty(); 51 | 52 | coordinatingClock.runOnce(); 53 | 54 | assertThat(coordinatingClock.getCurrentTick()).hasValue(1500); 55 | assertThat(wasLeader.get()).isFalse(); 56 | } 57 | 58 | @Test 59 | public void itAdvancesAClockWhenKeyIsInThePast() { 60 | fdb.run(tr -> { 61 | tr.set(Tuple.from(CoordinatingClock.PREFIX, "session".getBytes(Charsets.UTF_8)).pack(), ByteArrayUtil.encodeInt(500)); 62 | return null; 63 | }); 64 | 65 | CoordinatingClock coordinatingClock = CoordinatingClock.from( 66 | fdb, 67 | "session", 68 | () -> wasLeader.set(true), 69 | Clock.fixed(Instant.ofEpochMilli(1234), ZoneId.systemDefault()), 70 | OptionalLong.of(500)); 71 | 72 | assertThat(coordinatingClock.getCurrentTick()).hasValue(500); 73 | 74 | coordinatingClock.runOnce(); 75 | 76 | assertThat(coordinatingClock.getCurrentTick()).hasValue(1500); 77 | assertThat(wasLeader.get()).isFalse(); 78 | } 79 | 80 | @Test 81 | public void itWaitsAndAdvancesClockWhenKeyIsInTheFuture() { 82 | fdb.run(tr -> { 83 | tr.set(Tuple.from(CoordinatingClock.PREFIX, "session".getBytes(Charsets.UTF_8)).pack(), ByteArrayUtil.encodeInt(1234)); 84 | return null; 85 | }); 86 | 87 | CoordinatingClock coordinatingClock = CoordinatingClock.from( 88 | fdb, 89 | "session", 90 | () -> wasLeader.set(true), 91 | Clock.fixed(Instant.ofEpochMilli(1234), ZoneId.systemDefault()), 92 | OptionalLong.of(500)); 93 | 94 | assertThat(coordinatingClock.getCurrentTick()).hasValue(1234); 95 | 96 | coordinatingClock.runOnce(); 97 | 98 | assertThat(coordinatingClock.getCurrentTick()).hasValue(1500); 99 | assertThat(wasLeader.get()).isTrue(); 100 | } 101 | 102 | @Test 103 | public void itAdvancesManyTimes() { 104 | CoordinatingClock coordinatingClock = CoordinatingClock.from( 105 | fdb, 106 | "session", 107 | () -> wasLeader.set(true), 108 | Clock.fixed(Instant.ofEpochMilli(1050), ZoneId.systemDefault()), 109 | OptionalLong.of(100)); 110 | 111 | assertThat(coordinatingClock.getCurrentTick()).isEmpty(); 112 | 113 | coordinatingClock.runOnce(); 114 | assertThat(coordinatingClock.getCurrentTick()).hasValue(1100); 115 | assertThat(wasLeader.get()).isFalse(); 116 | 117 | coordinatingClock.runOnce(); 118 | assertThat(coordinatingClock.getCurrentTick()).hasValue(1200); 119 | assertThat(wasLeader.get()).isTrue(); 120 | 121 | coordinatingClock.runOnce(); 122 | assertThat(coordinatingClock.getCurrentTick()).hasValue(1300); 123 | assertThat(wasLeader.get()).isTrue(); 124 | 125 | coordinatingClock.runOnce(); 126 | assertThat(coordinatingClock.getCurrentTick()).hasValue(1400); 127 | assertThat(wasLeader.get()).isTrue(); 128 | } 129 | 130 | @Test 131 | public void itOnlyLetsOneClockBecomeTheLeaderBestEffort() { 132 | fdb.run(tr -> { 133 | tr.set(Tuple.from(CoordinatingClock.PREFIX, "session".getBytes(Charsets.UTF_8)).pack(), ByteArrayUtil.encodeInt(1000)); 134 | return null; 135 | }); 136 | 137 | AtomicBoolean clockTwoWasLeader = new AtomicBoolean(false); 138 | AtomicBoolean clockThreeWasLeader = new AtomicBoolean(false); 139 | 140 | CoordinatingClock coordinatingClockOne = CoordinatingClock.from( 141 | fdb, 142 | "session", 143 | () -> wasLeader.set(true), 144 | Clock.fixed(Instant.ofEpochMilli(1000), ZoneId.systemDefault()), 145 | OptionalLong.of(500)); 146 | 147 | CoordinatingClock coordinatingClockTwo = CoordinatingClock.from( 148 | fdb, 149 | "session", 150 | () -> clockTwoWasLeader.set(true), 151 | Clock.fixed(Instant.ofEpochMilli(1000), ZoneId.systemDefault()), 152 | OptionalLong.of(500)); 153 | 154 | CoordinatingClock coordinatingClockThree = CoordinatingClock.from( 155 | fdb, 156 | "session", 157 | () -> clockThreeWasLeader.set(true), 158 | Clock.fixed(Instant.ofEpochMilli(1000), ZoneId.systemDefault()), 159 | OptionalLong.of(500)); 160 | 161 | List clocks = ImmutableList.of(coordinatingClockOne, coordinatingClockTwo, coordinatingClockThree); 162 | 163 | clocks.stream() 164 | .map(clock -> CompletableFuture.runAsync(clock::runOnce)) 165 | .collect(Collectors.toList()) 166 | .forEach(CompletableFuture::join); 167 | 168 | // it's possible this could fail with an unfortunate scheduling of the futures, where they wind up running serially 169 | assertThat(wasLeader.get() ^ clockTwoWasLeader.get() ^ clockThreeWasLeader.get()).isTrue(); 170 | assertThat(coordinatingClockOne.getCurrentTick()).hasValue(1500); 171 | 172 | wasLeader.set(false); 173 | clockTwoWasLeader.set(false); 174 | clockThreeWasLeader.set(false); 175 | 176 | clocks.stream() 177 | .map(clock -> CompletableFuture.runAsync(clock::runOnce)) 178 | .collect(Collectors.toList()) 179 | .forEach(CompletableFuture::join); 180 | 181 | assertThat(wasLeader.get() ^ clockTwoWasLeader.get() ^ clockThreeWasLeader.get()).isTrue(); 182 | assertThat(coordinatingClockOne.getCurrentTick()).hasValue(2000); 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/session/FdbSessionManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.session; 2 | 3 | import static com.ph14.fdb.zk.session.FdbSessionManager.SESSION_DIRECTORY; 4 | import static com.ph14.fdb.zk.session.FdbSessionManager.SESSION_TIMEOUT_DIRECTORY; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 7 | 8 | import java.time.Clock; 9 | import java.time.Instant; 10 | import java.time.ZoneId; 11 | import java.util.OptionalLong; 12 | 13 | import org.apache.zookeeper.KeeperException.SessionExpiredException; 14 | import org.apache.zookeeper.KeeperException.SessionMovedException; 15 | import org.junit.Before; 16 | import org.junit.Test; 17 | 18 | import com.apple.foundationdb.directory.DirectoryLayer; 19 | import com.ph14.fdb.zk.FdbBaseTest; 20 | 21 | public class FdbSessionManagerTest extends FdbBaseTest { 22 | 23 | @Override 24 | @Before 25 | public void setUp() { 26 | super.setUp(); 27 | fdb.run(tr -> DirectoryLayer.getDefault().remove(tr, SESSION_DIRECTORY).join()); 28 | fdb.run(tr -> DirectoryLayer.getDefault().remove(tr, SESSION_TIMEOUT_DIRECTORY).join()); 29 | } 30 | 31 | @Test 32 | public void itCreatesAndReadsNewSession() { 33 | FdbSessionManager fdbSessionManager = new FdbSessionManager( 34 | fdb, 35 | Clock.fixed(Instant.ofEpochMilli(1234), ZoneId.systemDefault()), 36 | OptionalLong.of(200)); 37 | 38 | long session = fdbSessionManager.createSession(1000); 39 | long sessionTwo = fdbSessionManager.createSession(1201); 40 | 41 | assertThat(sessionTwo).isGreaterThan(session); 42 | 43 | long expiresAt = fdbSessionManager.getSessionDataById(session).getLong(0); 44 | long sessionTimeout = fdbSessionManager.getSessionDataById(session).getLong(1); 45 | assertThat(expiresAt).isEqualTo(2400); // timeout is 1000, current time 1234 --> 2234, rounded up to next tick of 200 --> 2400 46 | assertThat(sessionTimeout).isEqualTo(1000); 47 | 48 | expiresAt = fdbSessionManager.getSessionDataById(sessionTwo).getLong(0); 49 | sessionTimeout = fdbSessionManager.getSessionDataById(sessionTwo).getLong(1); 50 | assertThat(expiresAt).isEqualTo(2600); 51 | assertThat(sessionTimeout).isEqualTo(1201); 52 | } 53 | 54 | @Test 55 | public void itAddsKnownSession() { 56 | FdbSessionManager fdbSessionManager = new FdbSessionManager( 57 | fdb, 58 | Clock.fixed(Instant.ofEpochMilli(1234), ZoneId.systemDefault()), 59 | OptionalLong.of(200)); 60 | 61 | long sessionId = System.currentTimeMillis(); 62 | 63 | fdbSessionManager.addSession(sessionId, 1001); 64 | 65 | long expiresAt = fdbSessionManager.getSessionDataById(sessionId).getLong(0); 66 | long sessionTimeout = fdbSessionManager.getSessionDataById(sessionId).getLong(1); 67 | 68 | assertThat(expiresAt).isEqualTo(2400); 69 | assertThat(sessionTimeout).isEqualTo(1001); 70 | assertThat(fdbSessionManager.getExpiredSessionIds(2399)).isEmpty(); 71 | assertThat(fdbSessionManager.getExpiredSessionIds(2400)).containsExactly(sessionId); 72 | } 73 | 74 | @Test 75 | public void itCanUpdateExpirationWithHeartbeatAndClockAdvancement() { 76 | FdbSessionManager fdbSessionManager = new FdbSessionManager( 77 | fdb, 78 | Clock.fixed(Instant.ofEpochMilli(1234), ZoneId.systemDefault()), 79 | OptionalLong.of(200)); 80 | 81 | long session = fdbSessionManager.createSession(1000); 82 | assertThat(fdbSessionManager.getExpiredSessionIds(2401)).containsExactly(session); 83 | 84 | // advance our clock 85 | fdbSessionManager = new FdbSessionManager( 86 | fdb, 87 | Clock.fixed(Instant.ofEpochMilli(12345), ZoneId.systemDefault()), 88 | OptionalLong.of(200)); 89 | 90 | fdbSessionManager.touchSession(session, 500); 91 | 92 | long expiresAt = fdbSessionManager.getSessionDataById(session).getLong(0); 93 | long sessionTimeout = fdbSessionManager.getSessionDataById(session).getLong(1); 94 | 95 | assertThat(expiresAt).isEqualTo(13000); 96 | assertThat(sessionTimeout).isEqualTo(500); 97 | 98 | assertThat(fdbSessionManager.getExpiredSessionIds(2401)).isEmpty(); 99 | assertThat(fdbSessionManager.getExpiredSessionIds(13001)).containsExactly(session); 100 | } 101 | 102 | @Test 103 | public void itCanUpdateExpirationWithHeartbeatAndTimeoutAdvancement() { 104 | FdbSessionManager fdbSessionManager = new FdbSessionManager( 105 | fdb, 106 | Clock.fixed(Instant.ofEpochMilli(1234), ZoneId.systemDefault()), 107 | OptionalLong.of(200)); 108 | 109 | long session = fdbSessionManager.createSession(1000); 110 | assertThat(fdbSessionManager.getExpiredSessionIds(2401)).containsExactly(session); 111 | 112 | // embiggen the timeout 113 | fdbSessionManager.touchSession(session, 1500); 114 | 115 | long expiresAt = fdbSessionManager.getSessionDataById(session).getLong(0); 116 | long sessionTimeout = fdbSessionManager.getSessionDataById(session).getLong(1); 117 | 118 | assertThat(expiresAt).isEqualTo(2800); 119 | assertThat(sessionTimeout).isEqualTo(1500); 120 | 121 | assertThat(fdbSessionManager.getExpiredSessionIds(2401)).isEmpty(); 122 | assertThat(fdbSessionManager.getExpiredSessionIds(2800)).containsExactly(session); 123 | } 124 | 125 | @Test 126 | public void itDoesNothingWhenHeartbeatingWithSmallerExpiration() { 127 | FdbSessionManager fdbSessionManager = new FdbSessionManager( 128 | fdb, 129 | Clock.fixed(Instant.ofEpochMilli(1234), ZoneId.systemDefault()), 130 | OptionalLong.of(200)); 131 | 132 | long session = fdbSessionManager.createSession(1000); 133 | assertThat(fdbSessionManager.getExpiredSessionIds(2401)).containsExactly(session); 134 | 135 | fdbSessionManager.touchSession(session, 0); 136 | 137 | long expiresAt = fdbSessionManager.getSessionDataById(session).getLong(0); 138 | long sessionTimeout = fdbSessionManager.getSessionDataById(session).getLong(1); 139 | 140 | assertThat(expiresAt).isEqualTo(2400); 141 | assertThat(sessionTimeout).isEqualTo(1000); 142 | 143 | assertThat(fdbSessionManager.getExpiredSessionIds(2399)).isEmpty(); 144 | assertThat(fdbSessionManager.getExpiredSessionIds(2400)).containsExactly(session); 145 | } 146 | 147 | @Test 148 | public void itFindsExpiredNodes() { 149 | FdbSessionManager fdbSessionManager = new FdbSessionManager( 150 | fdb, 151 | Clock.fixed(Instant.ofEpochMilli(1234), ZoneId.systemDefault()), 152 | OptionalLong.of(200)); 153 | 154 | long session = fdbSessionManager.createSession(1000); 155 | assertThat(fdbSessionManager.getExpiredSessionIds(2399)).isEmpty(); 156 | assertThat(fdbSessionManager.getExpiredSessionIds(2400)).containsExactly(session); 157 | assertThat(fdbSessionManager.getExpiredSessionIds(2401)).containsExactly(session); 158 | assertThat(fdbSessionManager.getExpiredSessionIds(2402)).containsExactly(session); 159 | 160 | long sessionTwo = fdbSessionManager.createSession(1201); 161 | assertThat(fdbSessionManager.getExpiredSessionIds(2399)).isEmpty(); 162 | assertThat(fdbSessionManager.getExpiredSessionIds(2400)).containsExactly(session); 163 | assertThat(fdbSessionManager.getExpiredSessionIds(2401)).containsExactly(session); 164 | assertThat(fdbSessionManager.getExpiredSessionIds(2601)).containsExactly(session, sessionTwo); 165 | } 166 | 167 | @Test 168 | public void itThrowsWhenCheckingExpiredSession() throws SessionExpiredException, SessionMovedException { 169 | FdbSessionManager fdbSessionManager = new FdbSessionManager( 170 | fdb, 171 | Clock.fixed(Instant.ofEpochMilli(1234), ZoneId.systemDefault()), 172 | OptionalLong.of(200)); 173 | 174 | long session = fdbSessionManager.createSession(1000); 175 | 176 | fdbSessionManager.checkSession(session, null); 177 | 178 | // advance the clock 179 | fdbSessionManager = new FdbSessionManager( 180 | fdb, 181 | Clock.fixed(Instant.ofEpochMilli(2400), ZoneId.systemDefault()), 182 | OptionalLong.of(200)); 183 | 184 | FdbSessionManager finalFdbSessionManager = fdbSessionManager; 185 | assertThatThrownBy(() -> finalFdbSessionManager.checkSession(session, null)) 186 | .isInstanceOf(SessionExpiredException.class); 187 | } 188 | 189 | @Test 190 | public void itRemovesSessions() { 191 | FdbSessionManager fdbSessionManager = new FdbSessionManager( 192 | fdb, 193 | Clock.fixed(Instant.ofEpochMilli(1234), ZoneId.systemDefault()), 194 | OptionalLong.of(200)); 195 | 196 | long session = fdbSessionManager.createSession(1000); 197 | long sessionTwo = fdbSessionManager.createSession(1000); 198 | 199 | assertThat(fdbSessionManager.getExpiredSessionIds(2401)).containsExactly(session, sessionTwo); 200 | assertThat(fdbSessionManager.getSessionDataById(session).getLong(0)).isEqualTo(2400); 201 | 202 | fdbSessionManager.removeSession(session); 203 | 204 | assertThat(fdbSessionManager.getExpiredSessionIds(2401)).containsExactly(sessionTwo); 205 | assertThatThrownBy(() -> fdbSessionManager.getSessionDataById(session)).isInstanceOf(NullPointerException.class); 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /src/test/java/com/ph14/fdb/zk/watches/FdbWatchLocalIntegrationTests.java: -------------------------------------------------------------------------------- 1 | package com.ph14.fdb.zk.watches; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.Collections; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import org.apache.zookeeper.KeeperException; 9 | import org.apache.zookeeper.WatchedEvent; 10 | import org.apache.zookeeper.Watcher.Event.EventType; 11 | import org.apache.zookeeper.proto.CreateRequest; 12 | import org.apache.zookeeper.proto.CreateResponse; 13 | import org.apache.zookeeper.proto.GetDataRequest; 14 | import org.apache.zookeeper.proto.GetDataResponse; 15 | import org.apache.zookeeper.proto.SetDataRequest; 16 | import org.apache.zookeeper.proto.SetDataResponse; 17 | import org.junit.Test; 18 | 19 | import com.hubspot.algebra.Result; 20 | import com.ph14.fdb.zk.FdbBaseTest; 21 | 22 | public class FdbWatchLocalIntegrationTests extends FdbBaseTest { 23 | 24 | @Test 25 | public void itSetsAndFiresWatchForGetDataUpdates() throws InterruptedException { 26 | Result result = fdb.run( 27 | tr -> fdbCreateOp.execute(REQUEST, tr, new CreateRequest(BASE_PATH, "hello".getBytes(), Collections.emptyList(), 0))).join(); 28 | assertThat(result.unwrapOrElseThrow()).isEqualTo(new CreateResponse(BASE_PATH)); 29 | 30 | Result result2 = fdb.run( 31 | tr -> fdbGetDataOp.execute(REQUEST, tr, new GetDataRequest(BASE_PATH, true))).join(); 32 | assertThat(result2.isOk()).isTrue(); 33 | 34 | Result exists = fdb.run( 35 | tr -> fdbSetDataOp.execute(REQUEST, tr, new SetDataRequest(BASE_PATH, "hello!".getBytes(), 0))).join(); 36 | assertThat(exists.isOk()).isTrue(); 37 | 38 | WatchedEvent event = SERVER_CNXN.getWatchedEvents().poll(1, TimeUnit.SECONDS); 39 | assertThat(event).isNotNull(); 40 | assertThat(event.getType()).isEqualTo(EventType.NodeDataChanged); 41 | assertThat(event.getPath()).isEqualTo(BASE_PATH); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/org/apache/zookeeper/server/MockFdbServerCnxn.java: -------------------------------------------------------------------------------- 1 | package org.apache.zookeeper.server; 2 | 3 | import java.io.IOException; 4 | import java.net.InetSocketAddress; 5 | import java.nio.ByteBuffer; 6 | import java.util.concurrent.ArrayBlockingQueue; 7 | import java.util.concurrent.BlockingQueue; 8 | 9 | import org.apache.jute.Record; 10 | import org.apache.zookeeper.WatchedEvent; 11 | import org.apache.zookeeper.proto.ReplyHeader; 12 | 13 | public class MockFdbServerCnxn extends ServerCnxn { 14 | 15 | private final BlockingQueue watchedEvents = new ArrayBlockingQueue<>(10); 16 | 17 | @Override 18 | public void sendResponse(ReplyHeader h, Record r, String tag) throws IOException { 19 | 20 | } 21 | 22 | @Override 23 | public void process(WatchedEvent event) { 24 | watchedEvents.add(event); 25 | } 26 | 27 | @Override 28 | protected ServerStats serverStats() { 29 | return null; 30 | } 31 | 32 | @Override 33 | public long getOutstandingRequests() { 34 | return 0; 35 | } 36 | 37 | @Override 38 | public InetSocketAddress getRemoteSocketAddress() { 39 | return null; 40 | } 41 | 42 | @Override 43 | public int getInterestOps() { 44 | return 0; 45 | } 46 | 47 | @Override 48 | int getSessionTimeout() { 49 | return 0; 50 | } 51 | 52 | @Override 53 | void close() { 54 | 55 | } 56 | 57 | @Override 58 | void sendCloseSession() { 59 | 60 | } 61 | 62 | @Override 63 | long getSessionId() { 64 | return 0; 65 | } 66 | 67 | @Override 68 | void setSessionId(long sessionId) { 69 | 70 | } 71 | 72 | @Override 73 | void sendBuffer(ByteBuffer closeConn) { 74 | 75 | } 76 | 77 | @Override 78 | void enableRecv() { 79 | 80 | } 81 | 82 | @Override 83 | void disableRecv() { 84 | 85 | } 86 | 87 | @Override 88 | void setSessionTimeout(int sessionTimeout) { 89 | 90 | } 91 | 92 | public BlockingQueue getWatchedEvents() { 93 | return watchedEvents; 94 | } 95 | 96 | public void clearWatchedEvents() { 97 | watchedEvents.clear(); 98 | } 99 | 100 | } 101 | --------------------------------------------------------------------------------