├── .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, KeeperException> 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, KeeperException> 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 |
--------------------------------------------------------------------------------