diff --git a/.github/workflows/Helix-PR-CI.yml b/.github/workflows/Helix-PR-CI.yml index c061c5ecfe..d0ef2752c2 100644 --- a/.github/workflows/Helix-PR-CI.yml +++ b/.github/workflows/Helix-PR-CI.yml @@ -1,7 +1,7 @@ name: Helix PR CI on: pull_request: - branches: [ master ] + branches: [ master, metaclient ] # TODO: remove side branch paths-ignore: - '.github/**' - 'helix-front/**' diff --git a/bump-up.sh b/bump-up.sh index 9b23aec110..7190895983 100755 --- a/bump-up.sh +++ b/bump-up.sh @@ -74,7 +74,7 @@ echo "bump up: $current_version -> $new_version" update_pom_version "pom.xml" $current_version for module in "metrics-common" "metadata-store-directory-common" "zookeeper-api" "helix-common" "helix-core" \ - "helix-admin-webapp" "helix-rest" "helix-lock" "helix-view-aggregator" "helix-agent"; do + "helix-admin-webapp" "helix-rest" "helix-lock" "helix-view-aggregator" "helix-agent" "meta-client"; do update_ivy $module update_pom_version $module/pom.xml $current_version done diff --git a/meta-client/LICENSE b/meta-client/LICENSE new file mode 100644 index 0000000000..d78ae52e78 --- /dev/null +++ b/meta-client/LICENSE @@ -0,0 +1,270 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +For xstream: + +Copyright (c) 2003-2006, Joe Walnes +Copyright (c) 2006-2009, 2011 XStream Committers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of +conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of +conditions and the following disclaimer in the documentation and/or other materials provided +with the distribution. + +3. Neither the name of XStream nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +for jline: + +Copyright (c) 2002-2006, Marc Prud'hommeaux +All rights reserved. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the following +conditions are met: + +Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with +the distribution. + +Neither the name of JLine nor the names of its contributors +may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/meta-client/NOTICE b/meta-client/NOTICE new file mode 100644 index 0000000000..ca4a5e06ba --- /dev/null +++ b/meta-client/NOTICE @@ -0,0 +1,37 @@ +Apache Helix +Copyright 2014 The Apache Software Foundation + + +I. Included Software + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +Codehaus (http://www.codehaus.org/). +Licensed under the BSD License. + +This product includes software developed at +jline (http://jline.sourceforge.net/). +Licensed under the BSD License. + +This product includes software developed at +restlet (http://www.restlet.org/about/legal). +Licensed under the Apache License 2.0. + +This product includes software developed at +Google (http://www.google.com/). +Licensed under the Apache License 2.0. + +This product includes software developed at +snakeyaml (http://www.snakeyaml.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +zkclient (https://github.com/sgroschupf/zkclient). +Licensed under the Apache License 2.0. + +II. License Summary +- Apache License 2.0 +- BSD License diff --git a/meta-client/meta-client-1.0.5-SNAPSHOT.ivy b/meta-client/meta-client-1.0.5-SNAPSHOT.ivy new file mode 100644 index 0000000000..d845e4eb93 --- /dev/null +++ b/meta-client/meta-client-1.0.5-SNAPSHOT.ivy @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/meta-client/pom.xml b/meta-client/pom.xml new file mode 100644 index 0000000000..0c2a384b49 --- /dev/null +++ b/meta-client/pom.xml @@ -0,0 +1,108 @@ + + + + + org.apache.helix + helix + 1.2.1-SNAPSHOT + + 4.0.0 + + meta-client + bundle + Apache Helix :: Meta Client + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + Apache Software Foundation + http://www.apache.org + + + + + org.apache.commons.cli*, + * + + org.apache.helix.metaclient*;version="${project.version};-noimport:=true + + + + + org.apache.helix + zookeeper-api + ${project.version} + + + org.testng + testng + test + + + commons-io + commons-io + 2.11.0 + test + + + org.xerial.snappy + snappy-java + 1.1.7 + test + + + org.mockito + mockito-core + test + + + + + + ${basedir} + + DISCLAIMER + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + src/assemble/assembly.xml + + + + + package + + + + + + \ No newline at end of file diff --git a/meta-client/src/assemble/assembly.xml b/meta-client/src/assemble/assembly.xml new file mode 100644 index 0000000000..cd4eb210c6 --- /dev/null +++ b/meta-client/src/assemble/assembly.xml @@ -0,0 +1,60 @@ + + + + pkg + + tar + + + + ${project.build.directory}/${project.artifactId}-pkg/bin + bin + unix + 0755 + 0755 + + + ${project.build.directory}/${project.artifactId}-pkg/repo/ + repo + 0755 + 0755 + + **/*.xml + + + + ${project.build.directory}/${project.artifactId}-pkg/conf + conf + unix + 0755 + 0755 + + + ${project.basedir} + / + + LICENSE + NOTICE + DISCLAIMER + + 0755 + + + \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/api/AsyncCallback.java b/meta-client/src/main/java/org/apache/helix/metaclient/api/AsyncCallback.java new file mode 100644 index 0000000000..ae7aed59b9 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/api/AsyncCallback.java @@ -0,0 +1,78 @@ +package org.apache.helix.metaclient.api; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.List; +import javax.annotation.Nullable; + + +/** + * An asynchronous callback is deferred to invoke after an async CRUD operation finish and return. + * The corresponding callback is registered when async CRUD API is invoked. Implementation processes + * the result of each CRUD call. It should check return code and perform accordingly. + */ +public interface AsyncCallback { + + //This callback is used when stat object is returned from the operation. + interface StatCallback extends AsyncCallback { + /** + * Process the result of asynchronous calls that returns a stat object. + * @param returnCode The return code of the call. + * @param key the key that passed to asynchronous calls. + * @param stat the stats of the entry of the given key, returned from the async call. + * Could be null if the entry did not exist. + */ + void processResult(int returnCode, String key, @Nullable MetaClientInterface.Stat stat); + } + + //This callback is used when data is returned from the operation. + interface DataCallback extends AsyncCallback { + /** + * Process the result of asynchronous calls that returns entry data. + * @param returnCode The return code of the call. + * @param key The key that passed to asynchronous calls. + * @param data returned entry data from the call. + * @param stat the stats of the entry of the given key. Could be null if the entry did not exist. + */ + void processResult(int returnCode, String key, byte[] data, @Nullable MetaClientInterface.Stat stat); + } + + //This callback is used when nothing is returned from the operation. + interface VoidCallback extends AsyncCallback { + /** + * Process the result of asynchronous calls that has no return value. + * @param returnCode The return code of the call. + * @param key he key that passed to asynchronous calls. + */ + void processResult(int returnCode, String key); + } + + //This callback is used to process the list if OpResults from a single transactional call. + interface TransactionCallback extends AsyncCallback { + /** + * Process the result of asynchronous transactional calls. + * @param returnCode The return code of the transaction call. + * @param keys List of keys passed to the async transactional call. + * @param opResults The list of transactional results. + */ + void processResult(int returnCode, List keys, List opResults); + } + +} \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/api/ChildChangeListener.java b/meta-client/src/main/java/org/apache/helix/metaclient/api/ChildChangeListener.java new file mode 100644 index 0000000000..4d09450da9 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/api/ChildChangeListener.java @@ -0,0 +1,39 @@ +package org.apache.helix.metaclient.api; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Listener interface for children change events on a particular key. It includes new child + * creation, child deletion, child data change. + * TODO: add type for persist listener is removed + * For hierarchy key spaces like zookeeper, it refers to an entry's entire subtree. + * For flat key spaces, it refers to keys that matches `prefix*`. + */ +public interface ChildChangeListener { + enum ChangeType { + ENTRY_CREATED, // Any child entry created + ENTRY_DELETED, // Any child entry deleted + ENTRY_DATA_CHANGE // Any child entry has value change + } + /** + * Called when any child of the current key has changed. + */ + void handleChildChange(String changedPath, ChangeType changeType) throws Exception; +} \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/api/ConnectStateChangeListener.java b/meta-client/src/main/java/org/apache/helix/metaclient/api/ConnectStateChangeListener.java new file mode 100644 index 0000000000..63e7ae4e87 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/api/ConnectStateChangeListener.java @@ -0,0 +1,39 @@ +package org.apache.helix.metaclient.api; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +public interface ConnectStateChangeListener { + /** + * Called when the connection state has changed. I + * @param prevState previous state before state change event. + * @param currentState client state after state change event. If it is a one time listsner, it is + * possible that the metaclient state changes again + */ + void handleConnectStateChanged(MetaClientInterface.ConnectState prevState, MetaClientInterface.ConnectState currentState) throws Exception; + + /** + * Called when new connection failed to established. + * @param error error returned from metaclient or metadata service. + */ + void handleConnectionEstablishmentError(final Throwable error) throws Exception; + + // TODO: Consider add a callback for new connection when we add support for session ID. + +} \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/api/DataChangeListener.java b/meta-client/src/main/java/org/apache/helix/metaclient/api/DataChangeListener.java new file mode 100644 index 0000000000..84d11b34fa --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/api/DataChangeListener.java @@ -0,0 +1,33 @@ +package org.apache.helix.metaclient.api; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Listener interface for events on a particular key, including entry creating, deleting and value change. + */ +public interface DataChangeListener { + enum ChangeType { + ENTRY_CREATED, // Entry created of the specific path that the listener register to + ENTRY_DELETED, // Entry deleted of the specific path that the listener register to + ENTRY_UPDATE // Entry value updated + } + + void handleDataChange(String key, Object data, ChangeType changeType) throws Exception; +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/api/DataUpdater.java b/meta-client/src/main/java/org/apache/helix/metaclient/api/DataUpdater.java new file mode 100644 index 0000000000..6ad45d6426 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/api/DataUpdater.java @@ -0,0 +1,28 @@ +package org.apache.helix.metaclient.api; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Updates the value of a key. This is used together with {@link MetaClientInterface.update(String key, DataUpdater updater)}. + * @param + */ +public interface DataUpdater { + public T update(T currentData); +} \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/api/DirectChildChangeListener.java b/meta-client/src/main/java/org/apache/helix/metaclient/api/DirectChildChangeListener.java new file mode 100644 index 0000000000..83e67b8e62 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/api/DirectChildChangeListener.java @@ -0,0 +1,37 @@ +package org.apache.helix.metaclient.api; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Listener interface for direct child change event on a particular key. It includes new + * child creation or child deletion. The callback won't differentiate these types. + * For hierarchy key spaces like zookeeper, it refers to an entry's direct child nodes. + * For flat key spaces, it refers to keys that matches `prefix*separator`. + */ +public interface DirectChildChangeListener { + /** + * Called when there is a direct child entry creation or deleted. + * @param key The parent key where child change listener is subscribed. It would be the key + * passed to subscribeDirectChildChange. + * @throws Exception + */ + void handleDirectChildChange(String key) throws Exception; + +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/api/DirectChildSubscribeResult.java b/meta-client/src/main/java/org/apache/helix/metaclient/api/DirectChildSubscribeResult.java new file mode 100644 index 0000000000..6d45954b53 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/api/DirectChildSubscribeResult.java @@ -0,0 +1,45 @@ +package org.apache.helix.metaclient.api; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.List; + + +public class DirectChildSubscribeResult { + // A list of direct children names at the time when change is subscribed. + // It includes only one level child name, does not include further sub children names. + private final List _children; + + // true means the listener is registered successfully. + private final boolean _isRegistered; + + public DirectChildSubscribeResult(List children, boolean isRegistered) { + _children = children; + _isRegistered = isRegistered; + } + + public List getDirectChildren() { + return _children; + } + + public boolean isRegistered() { + return _isRegistered; + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/api/MetaClientInterface.java b/meta-client/src/main/java/org/apache/helix/metaclient/api/MetaClientInterface.java new file mode 100644 index 0000000000..375c0814c3 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/api/MetaClientInterface.java @@ -0,0 +1,583 @@ +package org.apache.helix.metaclient.api; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.NotImplementedException; +import org.apache.helix.metaclient.exception.MetaClientInterruptException; +import org.apache.helix.metaclient.exception.MetaClientTimeoutException; + + +public interface MetaClientInterface { + + enum EntryMode { + // The node will be removed automatically when the session associated with the creation + // of the node expires. + EPHEMERAL, + + // The node will not be automatically deleted upon client's disconnect. + // An ephemeral node cannot have sub entry. + PERSISTENT, + + // For metadata storage that has hierarchical key space (e.g. ZK), the node will be + // automatically deleted at some point in the future if the last child of the node is deleted. + // For metadata storage that has non-hierarchical key space (e.g. etcd), the node will be + // automatically deleted at some point in the future if the last entry that has the prefix + // is deleted. + // The node is an ephemeral node. + CONTAINER, + + // For metadata storage that has hierarchical key space (e.g. ZK) If the entry is not modified + // within the TTL and has no children it will become a candidate to be deleted by the server + // at some point in the future. + // For metadata storage that has non-hierarchical key space (e.g. etcd) If the entry is not modified + // within the TTL, it will become a candidate to be deleted by the server at some point in the + // future. + TTL + } + + enum ConnectState { + // Client is not connected to server. Before initiating connection or after close. + NOT_CONNECTED, + + // Client is connected to server + CONNECTED, + + // Authentication failed. + AUTH_FAILED, + + // Server has expired this connection. + EXPIRED, + + // When client explicitly call disconnect. + CLOSED_BY_CLIENT, + + // Connection between client and server is lost. + DISCONNECTED, + + // Client is authenticated. They can perform operation with authorized permissions. + // This state is not in use as of now. + AUTHENTICATED + } + + /** + * Interface representing the metadata of an entry. It contains entry type and version number. + * TODO: we will add session ID to entry stats in the future + * TODO: Add support for expiry time + */ + class Stat { + private final int _version; + private final EntryMode _entryMode; + // The expiry time of a TTL node in milliseconds. The default is -1 for nodes without expiry time. + private long _expiryTime; + + // The time when the node is created. Measured in milliseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC). + private long _creationTime; + + // The time when the node was las modified. Measured in milliseconds since the Unix epoch when the node was last modified. + private long _modifiedTime; + + public EntryMode getEntryType() { + return _entryMode; + } + + public int getVersion() { + return _version; + } + + public long getExpiryTime() { + return _expiryTime; + } + + public long getCreationTime() { + return _creationTime; + } + + public long getModifiedTime() { + return _modifiedTime; + } + + public Stat (EntryMode mode, int version) { + _version = version; + _entryMode = mode; + _expiryTime = -1; + } + + public Stat (EntryMode mode, int version, long ctime, long mtime, long etime) { + _version = version; + _entryMode = mode; + _creationTime = ctime; + _modifiedTime = mtime; + _expiryTime = etime; + } + } + + //synced CRUD API + + /** + * Create a persistent entry with given key and data. The entry will not be created if there is + * an existing entry with the same key. + * @param key key to identify the entry + * @param data value of the entry + */ + void create(final String key, T data); + + /** + * Create an entry of given EntryMode with given key and data. The entry will not be created if + * there is an existing entry with ethe same key. + * @param key key to identify the entry + * @param data value of the entry + * @param mode EntryMode identifying if the entry will be deleted upon client disconnect + */ + void create(final String key, final T data, final EntryMode mode); + + /** + * Create an entry of given EntryMode with given key, data, and expiry time (ttl). + * The entry will automatically purge when reached expiry time and has no children. + * The entry will not be created if there is an existing entry with the same key. + * @param key key to identify the entry + * @param data value of the entry + * @param ttl Time-to-live value of the node in milliseconds. + */ + void createWithTTL(final String key, final T data, final long ttl); + + /** + * Renews the specified TTL node adding its original expiry time + * to the current time. Throws an exception if the key is not a valid path + * or isn't of type TTL. + * @param key key to identify the entry + */ + void renewTTLNode(final String key); + + /** + * Set the data for the entry of the given key if it exists and the given version matches the + * version of the node (if the given version is -1, it matches any node's versions). + * @param key key to identify the entry + * @param data new data of the entry + * @param version expected version of the entry. -1 matched any version. + */ + void set(final String key, final T data, int version); + + /** + * Update existing data of a given key using an updater. This method will issue a read to get + * current data and apply updater upon the current data. + * @param key key to identify the entry + * @param updater An updater that modifies the entry value. + * @return the updated value. + */ + T update(final String key, DataUpdater updater); + + /** + * Check if there is an entry for the given key. + * @param key key to identify the entry + * @return return a Stat object if the entry exists. Return null otherwise. + */ + Stat exists(final String key); + + /** + * Fetch the data for a given key. + * TODO: define exception type when key does not exist + * @param key key to identify the entry + * @return Return data of the entry + */ + T get(final String key); + + /** + * API for transaction. The list of operation will be executed as an atomic operation. + * @param ops a list of operations. These operations will all be executed or none of them. + * @return Return a list of OpResult. + */ + List transactionOP(final Iterable ops); + + /** + * Return a list of children for the given keys. + * @param key For metadata storage that has hierarchical key space (e.g. ZK), the key would be + * a parent key, + * For metadata storage that has non-hierarchical key space (e.g. etcd), the key would + * be a prefix key. + * @return Return a list of children keys. Return direct child name only for hierarchical key + * space, return the whole sub key for non-hierarchical key space. + */ + List getDirectChildrenKeys(final String key); + + /** + * Return the number of children for the given keys. + * @param key For metadata storage that has hierarchical key space (e.g. ZK), the key would be + * a parent key, + * For metadata storage that has non-hierarchical key space (e.g. etcd), the key would + * be a prefix key. + */ + int countDirectChildren(final String key); + + /** + * Remove the entry associated with the given key. + * For metadata storage that has hierarchical key space, the entry can only be deleted if the key + * has no child entry. + * TODO: define exception to throw + * @param key key to identify the entry to delete + * @return Return true if the deletion is completed + */ + boolean delete(final String key); + + /** + * Remove the entry associated with the given key. + * For metadata storage that has hierarchical key space, remove all its child entries as well + * For metadata storage that has non-hierarchical key space, this API is the same as delete() + * @param key key to identify the entry to delete + * @return Return true if the deletion is completed + */ + boolean recursiveDelete(final String key); + + /* Asynchronous methods return immediately. + * They take a callback object that will be executed either on successful execution of the request + * or on error with an appropriate return code indicating the error. + */ + + /** + * User may register callbacks for async CRUD calls. These callbacks will be executed in a async + * thread pool. User could define the thread pool size. Default value is 10. + * TODO: add const default value in a separate file + * @param poolSize pool size for executing user resisted async callbacks + */ + void setAsyncExecPoolSize(int poolSize); + + /** + * The asynchronous version of create. + * @param key key to identify the entry + * @param data value of the entry + * @param mode EntryMode identifying if the entry will be deleted upon client disconnect + * @param cb A user defined VoidCallback implementation that will be invoked when async create return. + * @see org.apache.helix.metaclient.api.AsyncCallback.VoidCallback + */ + void asyncCreate(final String key, final T data, final EntryMode mode, + AsyncCallback.VoidCallback cb); + + /** + * The asynchronous version of set. + * @param key key to identify the entry + * @param data new data of the entry + * @param version expected version if the entry. -1 matched any version + * @param cb A user defined VoidCallback implementation that will be invoked when async create return. + * @see org.apache.helix.metaclient.api.AsyncCallback.StatCallback + */ + void asyncSet(final String key, final T data, final int version, AsyncCallback.StatCallback cb); + + /** + * The asynchronous version of update. + * @param key key to identify the entry + * @param updater An updater that modifies the entry value. + * @param cb A user defined VoidCallback implementation that will be invoked when async create return. + * It will contain the newly updated data if update succeeded. + * @see org.apache.helix.metaclient.api.AsyncCallback.DataCallback + */ + void asyncUpdate(final String key, DataUpdater updater, AsyncCallback.DataCallback cb); + + /** + * The asynchronous version of get. + * @param key key to identify the entry + * @param cb A user defined VoidCallback implementation that will be invoked when async get return. + * It will contain the entry data if get succeeded. + * @see org.apache.helix.metaclient.api.AsyncCallback.DataCallback + */ + void asyncGet(final String key, AsyncCallback.DataCallback cb); + + /** + * The asynchronous version of get sub entries. + * @param key key to identify the entry + * @param cb A user defined VoidCallback implementation that will be invoked when async count child return. + * It will contain the list of child keys if succeeded. + * @see org.apache.helix.metaclient.api.AsyncCallback.DataCallback + */ + void asyncCountChildren(final String key, AsyncCallback.DataCallback cb); + + /** + * The asynchronous version of get sub entries. + * @param key key to identify the entry + * @param cb A user defined VoidCallback implementation that will be invoked when async exist return. + * It will contain the stats of the entry if succeeded. + * @see org.apache.helix.metaclient.api.AsyncCallback.StatCallback + */ + void asyncExist(final String key, AsyncCallback.StatCallback cb); + + /** + * The asynchronous version of delete. + * @param key key to identify the entry + * @param cb A user defined VoidCallback implementation that will be invoked when async delete + * finish and return. @see org.apache.helix.metaclient.api.AsyncCallback.DataCallback + */ + void asyncDelete(final String key, AsyncCallback.VoidCallback cb); + + /** + * The asynchronous version of transaction operations. + * @param ops A list of operations + * @param cb A user defined TransactionCallback implementation that will be invoked when + * transaction operations finish and return. The TransactionCallback will contain + * either a list of OpResult if transaction finish successfully, or a return code + * indicating failure reason. @see org.apache.helix.metaclient.api.AsyncCallback.TransactionCallback + */ + void asyncTransaction(final Iterable ops, AsyncCallback.TransactionCallback cb); + + /* Batched APIs return result to user when all request finishes. + * These calls are not executed as a transaction. + */ + + /** + * Batch version of create. All entries will be created in persist mode. Returns when all request + * finishes. These calls are not executed as a transaction. + * @param key A list of key for create operations. + * @param data A list of data. Need to be in the same length of list of key. + * @return A list of boolean indicating create result of each operation. + */ + boolean[] create(List key, List data); + + /** + * Batch version of create. Returns when all request finishes. These calls are not executed as a + * transaction. + * @param key A list of key for create operations. + * @param data A list of data. Need to be in the same length of list of key. + * @param mode A list of EntryMode. Need to be in the same length of list of key. + * @return A list of boolean indicating create result of each operation. + */ + boolean[] create(List key, List data, List mode); + + /** + * Batch version of set. Returns when all request finishes. These calls are not executed as a + * transaction. + * @param keys A list of key for set operations. + * @param datas A list of data. Need to be in the same length of list of key. + * @param version A list of expected version of the entry. -1 matched any version. + * Need to be in the same length of list of key. + * @return A list of boolean indicating set result of each operation. + */ + boolean[] set(List keys, List datas, List version); + + /** + * Batch version of update. Returns when all request finishes. These calls are not executed as a + * transaction. + * @param keys A list of key for update operations. + * @param updater A list of updater. Need to be in the same length of list of key. + * @return A list of updated entry values. + */ + List update(List keys, List> updater); + + /** + * Batch version of get. Returns when all request finishes. These calls are not executed as a + * transaction. + * @param keys A list of key for get operations. + * @return A list of entry values. + */ + List get(List keys); + + /** + * Batch version of exists. Returns when all request finishes. These calls are not executed as a + * transaction. + * @param keys A list of key for exists operations. + * @return A list of stats for the given entries. + */ + List exists(List keys); + + /** + * Batch version of delete. Returns when all request finishes. These calls are not executed as a + * transaction. + * @param keys A list of key for delete operations. + * @return A list of boolean indicating delete result of each operation. + */ + boolean[] delete(List keys); + + /** + * Maintains a connection with underlying metadata service based on config params. Connection + * created by this method will be used to perform CRUD operations on metadata service. + * @throws MetaClientInterruptException + * if the connection timed out due to thread interruption + * @throws MetaClientTimeoutException + * if the connection timed out + * @throws IllegalStateException + * if already connected or the connection is already closed explicitly + */ + void connect(); + + /** + * Disconnect from server explicitly. + */ + void disconnect(); + + /** + * @return client current connection state with metadata service. + */ + ConnectState getClientConnectionState(); + + // Event notification APIs, user can register multiple listeners on the same key/connection state. + // All listeners will be automatically removed when client is disconnected. + // TODO: add auto re-register listener option + + /** + * Subscribe change of a particular entry. Including entry data change, entry deletion and creation + * of the given key. + * The listener should be permanent until it's unsubscribed. + * @param key Key to identify the entry + * @param listener An implementation of {@link org.apache.helix.metaclient.api.DataChangeListener} to register + * @param skipWatchingNonExistNode Will not register lister to a non-exist key if set to true. + * Please set to false if you are expecting ENTRY_CREATED type. + * @return Return a boolean indication if subscribe succeeded. + */ + boolean subscribeDataChange(String key, DataChangeListener listener, boolean skipWatchingNonExistNode); + + /** + * Subscribe a one-time change of a particular entry. Including entry data change, entry deletion and creation + * of the given key. + * The implementation should use at-most-once delivery semantic. + * @param key Key to identify the entry + * @param listener An implementation of {@link org.apache.helix.metaclient.api.DataChangeListener} to register + * @param skipWatchingNonExistNode Will not register lister to a non-exist key if set to true. + * Please set to false if you are expecting ENTRY_CREATED type. + * @return Return a boolean indication if subscribe succeeded. + */ + default boolean subscribeOneTimeDataChange(String key, DataChangeListener listener, + boolean skipWatchingNonExistNode) { + throw new NotImplementedException("subscribeOneTimeDataChange is not implemented"); + } + + /** + * Subscribe for direct child change event on a particular key. It includes new child + * creation or deletion. It does not include existing child data change. + * The listener should be permanent until it's unsubscribed. + * For hierarchy key spaces like zookeeper, it refers to an entry's direct children nodes. + * For flat key spaces, it refers to keys that matches `prefix*separator`. + * @param key key to identify the entry. + * @param listener An implementation of {@link org.apache.helix.metaclient.api.DirectChildChangeListener} to register + * @param skipWatchingNonExistNode If the passed in key does not exist, no listener wil be registered. + * + * @return Return a DirectChildSubscribeResult. It will contain a list of direct sub children if + * subscribe succeeded. + */ + DirectChildSubscribeResult subscribeDirectChildChange(String key, + DirectChildChangeListener listener, boolean skipWatchingNonExistNode); + + /** + * Subscribe for a one-time direct child change event on a particular key. It includes new child + * creation or deletion. It does not include existing child data change. + * The implementation should use at-most-once delivery semantic. + * For hierarchy key spaces like zookeeper, it refers to an entry's direct children nodes. + * For flat key spaces, it refers to keys that matches `prefix*separator`. + * + * @param key key to identify the entry. + * @param listener An implementation of {@link org.apache.helix.metaclient.api.DirectChildChangeListener} to register + * @param skipWatchingNonExistNode If the passed in key does not exist, no listener wil be registered. + * + * @return Return a DirectChildSubscribeResult. It will contain a list of direct sub children if + * subscribe succeeded. + */ + default DirectChildSubscribeResult subscribeOneTimeDirectChildChange(String key, + DirectChildChangeListener listener, boolean skipWatchingNonExistNode) { + throw new NotImplementedException("subscribeOneTimeDirectChildChange is not implemented"); + } + + /** + * Subscribe for connection state change. + * The listener should be permanent until it's unsubscribed. + * @param listener An implementation of {@link org.apache.helix.metaclient.api.ConnectStateChangeListener} to register + * + * @return Return a boolean indication if subscribe succeeded. + */ + boolean subscribeStateChanges(ConnectStateChangeListener listener); + + /** + * Subscribe change for all children including entry change and data change. + * The listener should be permanent until it's unsubscribed. + * For hierarchy key spaces like zookeeper, it would watch the whole tree structure. + * For flat key spaces, it would watch for keys with certain prefix. + * @param key key to identify the entry. + * @param listener An implementation of {@link org.apache.helix.metaclient.api.ChildChangeListener} to register + * @param skipWatchingNonExistNode If the passed in key does not exist, no listener wil be registered. + */ + boolean subscribeChildChanges(String key, ChildChangeListener listener, boolean skipWatchingNonExistNode); + + /** + * Subscribe a one-time change for all children including entry change and data change. + * The implementation should use at-most-once delivery semantic. + * For hierarchy key spaces like zookeeper, it would watch the whole tree structure. + * For flat key spaces, it would watch for keys with certain prefix. + * @param key key to identify the entry. + * @param listener An implementation of {@link org.apache.helix.metaclient.api.ChildChangeListener} to register + * @param skipWatchingNonExistNode If the passed in key does not exist, no listener wil be registered. + */ + default boolean subscribeOneTimeChildChanges(String key, ChildChangeListener listener, + boolean skipWatchingNonExistNode) { + throw new NotImplementedException("subscribeOneTimeChildChanges is not implemented"); + } + + /** + * Unsubscribe the listener to further changes. No-op if the listener is not subscribed to the key. + * @param key Key to identify the entry. + * @param listener The listener to unsubscribe. + */ + void unsubscribeDataChange(String key, DataChangeListener listener); + + /** + * Unsubscribe the listener to further changes. No-op if the listener is not subscribed to the key. + * @param key Key to identify the entry. + * @param listener The listener to unsubscribe. + */ + void unsubscribeDirectChildChange(String key, DirectChildChangeListener listener); + + /** + * Unsubscribe the listener to further changes. No-op if the listener is not subscribed to the key. + * @param key Key to identify the entry. + * @param listener The listener to unsubscribe. + */ + void unsubscribeChildChanges(String key, ChildChangeListener listener); + + /** + * Unsubscribe the listener to further changes. No-op if the listener is not subscribed to the key. + * @param listener The listener to unsubscribe. + */ + void unsubscribeConnectStateChanges(ConnectStateChangeListener listener); + + /** + * Block the call until the given key exists or timeout. + * @param key Key to monitor. + * @param timeUnit timeout unit + * @param timeOut timeout value + * @return + */ + boolean waitUntilExists(String key, TimeUnit timeUnit, long timeOut); + + /** + * Serialize the data in type T to a byte array. This function can be used in API that returns or + * has input value in byte array format. + * @param data to be serialized. + * @param path timeout unit + * @return + */ + byte[] serialize(T data, String path); + + /** + * Serialize a byte array to data in type T. This function can be used in API that returns or + * has input value in byte array format. + * @param bytes to be deserialized. + * @param path timeout unit + * @return + */ + T deserialize(byte[] bytes, String path); + + // TODO: Secure CRUD APIs +} \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/api/Op.java b/meta-client/src/main/java/org/apache/helix/metaclient/api/Op.java new file mode 100644 index 0000000000..4b4deffeb1 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/api/Op.java @@ -0,0 +1,136 @@ +package org.apache.helix.metaclient.api; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Represents a single operation in a multi-operation transaction. Each operation can be a create, set, + * version check or delete operation. + */ +public abstract class Op { + public enum Type { + CREATE, + DELETE, + SET, + CHECK + } + + private String _path; + private Type _type; + + private Op(Type type, String path) { + this._type = type; + this._path = path; + } + public static Op create(String path, byte[] data) { + return new Create(path, data); + } + + public static Op create(String path, byte[] data, MetaClientInterface.EntryMode createMode) { + return new Create(path, data, createMode); + } + + public static Op delete(String path, int version) { + return new Op.Delete(path, version); + } + + public static Op set(String path, byte[] data, int version) { + return new Set(path, data, version); + } + + public static Op check(String path, int version) { + return new Check(path, version); + } + + public Type getType() { + return this._type; + } + + public String getPath() { + return this._path; + } + + /** + * Check the version of an entry. True only when the version is the same as expected. + */ + public static class Check extends Op { + private final int version; + public int getVersion() { return version;} + private Check(String path, int version) { + super(Type.CHECK, path); + this.version = version; + } + } + + /** + * Represents a Create operation. Creates a new node. + */ + public static class Create extends Op { + protected final byte[] data; + private MetaClientInterface.EntryMode mode; + + public byte[] getData() { + return data; + } + public MetaClientInterface.EntryMode getEntryMode() {return mode;} + + private Create(String path, byte[] data) { + super(Type.CREATE, path); + this.data = data; + } + + private Create(String path, byte[] data, MetaClientInterface.EntryMode mode) { + super(Type.CREATE, path); + this.data = data; + this.mode = mode; + } + } + + /** + * Represents a Delete operations. Deletes an existing node. + */ + public static class Delete extends Op{ + private final int version; + public int getVersion() { return version;} + + private Delete(String path, int version) { + super(Type.DELETE, path); + this.version = version; + } + } + + /** + * Represents a Set operation. Sets or updates the data of a node. + */ + public static class Set extends Op { + private final byte[] data; + private final int version; + + public byte[] getData() { + return data; + } + public int getVersion() { return version;} + + private Set(String path, byte[] data, int version) { + super(Type.SET, path); + this.data = data; + this.version = version; + } + } +} \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/api/OpResult.java b/meta-client/src/main/java/org/apache/helix/metaclient/api/OpResult.java new file mode 100644 index 0000000000..effed85433 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/api/OpResult.java @@ -0,0 +1,167 @@ +package org.apache.helix.metaclient.api; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.Arrays; +import java.util.List; +/** + * Represent the result of a single operation of a multi operation transaction. + */ +public class OpResult { + + enum Type { + ERRORRESULT, + GETDATARESULT, + GETCHILDRENRESULT, + CHECKRESULT, + SETDATARESULT, + DELETERESULT, + CREATERESULT, + CREATERESULT_WITH_STAT + } + + private Type type; + + private OpResult(Type type) { + this.type = type; + } + + public Type getType() { + return this.type; + } + + /** + * Represents the result of an operation that was attempted to execute but failed. + */ + public static class ErrorResult extends OpResult { + private int err; + + public ErrorResult(int err) { + super(Type.ERRORRESULT); + this.err = err; + } + + public int getErr() { + return this.err; + } + } + + /** + * Represents the result of a getData() operation. + */ + public static class GetDataResult extends OpResult { + private byte[] data; + private MetaClientInterface.Stat stat; + + public GetDataResult(byte[] data, MetaClientInterface.Stat stat) { + super(Type.GETDATARESULT); + this.data = data == null ? null : Arrays.copyOf(data, data.length); + this.stat = stat; + } + + public byte[] getData() { + return this.data == null ? null : Arrays.copyOf(this.data, this.data.length); + } + + public MetaClientInterface.Stat getStat() { + return this.stat; + } + } + + /** + * Represents the result of a getChildren() operation. + */ + public static class GetChildrenResult extends OpResult { + private List children; + + public GetChildrenResult(List children) { + super(Type.GETCHILDRENRESULT); + this.children = children; + } + + public List getChildren() { + return this.children; + } + } + + /** + * Represents the result of a check() operation. + */ + public static class CheckResult extends OpResult { + public CheckResult() { + super(Type.CHECKRESULT); + } + } + + /** + * Represents the result of a set() operation. + */ + public static class SetDataResult extends OpResult { + private MetaClientInterface.Stat stat; + + public SetDataResult(MetaClientInterface.Stat stat) { + super(Type.SETDATARESULT); + this.stat = stat; + } + + public MetaClientInterface.Stat getStat() { + return this.stat; + } + } + + /** + * Represents the result of a delete() operation. + */ + public static class DeleteResult extends OpResult { + public DeleteResult() { + super(Type.DELETERESULT); + } + } + + /** + * Represents the result of a create() operation. + */ + public static class CreateResult extends OpResult { + private String path; + private MetaClientInterface.Stat stat; + + public CreateResult(String path) { + this(Type.CREATERESULT, path, null); + } + + public CreateResult(String path, MetaClientInterface.Stat stat) { + this(Type.CREATERESULT_WITH_STAT, path, stat); + } + + private CreateResult(Type type, String path, MetaClientInterface.Stat stat) { + super(type); + this.path = path; + this.stat = stat; + } + + public String getPath() { + return this.path; + } + + public MetaClientInterface.Stat getStat() { + return this.stat; + } + } +} \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/constants/MetaClientConstants.java b/meta-client/src/main/java/org/apache/helix/metaclient/constants/MetaClientConstants.java new file mode 100644 index 0000000000..d05c0ea5b4 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/constants/MetaClientConstants.java @@ -0,0 +1,49 @@ +package org.apache.helix.metaclient.constants; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +public final class MetaClientConstants { + + private MetaClientConstants(){ + + } + + // Stop retrying when we reach timeout + //TODO The value should be the same as Helix default ZK retry time. Modify when change #2293 merged + public static final int DEFAULT_OPERATION_RETRY_TIMEOUT_MS = Integer.MAX_VALUE; + + // maxMsToWaitUntilConnected + public static final int DEFAULT_CONNECTION_INIT_TIMEOUT_MS = 60 * 1000; + + // When a client becomes partitioned from the metadata service for more than session timeout, + // new session will be established. + public static final int DEFAULT_SESSION_TIMEOUT_MS = 30 * 1000; + + // Max backoff window for exponential reconnect back off policy. by default is 30 seconds. + public static final long DEFAULT_MAX_EXP_BACKOFF_RETRY_INTERVAL_MS = 30 * 1000; + + // Initial backoff window for exponential reconnect back off policy. by default is 500 ms. + public static final long DEFAULT_INIT_EXP_BACKOFF_RETRY_INTERVAL_MS = 500; + + // Auto Reconnect timeout + public static final long DEFAULT_AUTO_RECONNECT_TIMEOUT_MS = 30 * 60 * 1000; + + //public static final long DEFAULT_MAX_LINEAR_BACKOFF_RETRY_WINDOW_MS = 5*1000; +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/datamodel/DataRecord.java b/meta-client/src/main/java/org/apache/helix/metaclient/datamodel/DataRecord.java new file mode 100644 index 0000000000..3ed6928f17 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/datamodel/DataRecord.java @@ -0,0 +1,38 @@ +package org.apache.helix.metaclient.datamodel; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.apache.helix.zookeeper.datamodel.ZNRecord; + +/** + * The DataRecord object is a wrapper around ZNRecord. + * TODO: Create an interface to decouple DataRecord and have a pluggable record store. + */ +@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +public class DataRecord extends ZNRecord { + public DataRecord(String znodeId) { + super(znodeId); + } + + public DataRecord(ZNRecord record) { + super(record); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientBadVersionException.java b/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientBadVersionException.java new file mode 100644 index 0000000000..bc59a4ac7d --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientBadVersionException.java @@ -0,0 +1,20 @@ +package org.apache.helix.metaclient.exception; + +public final class MetaClientBadVersionException extends MetaClientException { + public MetaClientBadVersionException() { + super(); + } + + public MetaClientBadVersionException(String message, Throwable cause) { + super(message, cause); + } + + public MetaClientBadVersionException(String message) { + super(message); + } + + public MetaClientBadVersionException(Throwable cause) { + super(cause); + } + +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientException.java b/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientException.java new file mode 100644 index 0000000000..3854fadaff --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientException.java @@ -0,0 +1,138 @@ +package org.apache.helix.metaclient.exception; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +public class MetaClientException extends RuntimeException { + public MetaClientException() { + super(); + } + + public MetaClientException(String message, Throwable cause) { + super(message, cause); + } + + public MetaClientException(String message) { + super(message); + } + + public MetaClientException(Throwable cause) { + super(cause); + } + + public enum ReturnCode { + /** Connection to the server has been lost. */ + CONNECTION_LOSS(-105, "Connection to the server has been lost.") , + + /** Operation is unimplemented. */ + UNIMPLEMENTED(-104, "Operation is unimplemented."), + + /** Operation timeout. */ + OPERATION_TIMEOUT(-103, "Operation timeout.") { + @Override + public MetaClientException createMetaClientException() { + return new MetaClientTimeoutException(); + } + }, + + /** Either a runtime or data inconsistency was found. */ + CONSISTENCY_ERROR(-102, "Inconsistency was found."), + + /** Session is moved or expired or non-exist. */ + SESSION_ERROR(-101, "Session is moved or expired or non-exist."), + + /** Indicates a system and server-side errors not defined by following codes. + * It also indicate a range. Any value smaller or equal than this indicating error from + * server side. + */ + DB_SYSTEM_ERROR(-100, "System and server-side errors."), + + /** The listener does not exists. */ + INVALID_LISTENER(-9, "Listener does not exists."), + + /** Authentication failed. */ + AUTH_FAILED(-8, "authentication failed"), + + /** Invalid arguments. */ + INVALID_ARGUMENTS(-7, "Invalid arguments"), + + /** Version conflict. Return when caller tries to edit an entry with a specific version but + * the actual version of the entry on server is different. */ + BAD_VERSION(-6, "Version conflict.") { + @Override + public MetaClientException createMetaClientException() { + return new MetaClientBadVersionException(); + } + }, + + /** Entry already exists. Return when try to create a duplicated entry. */ + ENTRY_EXISTS(-5, "Entry already exists."), + + /** The client is not Authenticated. */ + NO_AUTH(-4, "Not Authenticated.") , + + /** Entry does not exist. */ + NO_SUCH_ENTRY(-3, "Entry does not exist.") { + @Override + public MetaClientException createMetaClientException() { + return new MetaClientNoNodeException(); + } + }, + + /**The entry has sub entries. Return when operation can only be down at entry with no + * sub entries. (i.e. unrecursively delete an entry . )*/ + NOT_LEAF_ENTRY(-2, "The entry has sub entries."), + + /** Indicates a system and DB client or a usage errors not defined by following codes. + * It also indicate a range. Any value smaller or equal than this and larger than + * DB_SYSTEM_ERROR indicating error from client or caused by wrong usage. + */ + DB_USER_ERROR(-1, "Client or usage error."), + + /** Everything is OK. */ + OK(0, "OK") { + @Override + public MetaClientException createMetaClientException() { + return null; + } + }; + + private final int _intValue; + private final String _message; + + ReturnCode(int codeIntValue, String message) { + _intValue = codeIntValue; + _message = message; + } + + public String getMessage() { + return _message; + } + + public int getIntValue() { + return _intValue; + } + + public MetaClientException createMetaClientException() { + // TODO: add more code translation when new exception class is created. + return new MetaClientException(); + } + } + +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientInterruptException.java b/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientInterruptException.java new file mode 100644 index 0000000000..60bc79607f --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientInterruptException.java @@ -0,0 +1,38 @@ +package org.apache.helix.metaclient.exception; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +public final class MetaClientInterruptException extends MetaClientException { + public MetaClientInterruptException() { + super(); + } + + public MetaClientInterruptException(String message, Throwable cause) { + super(message, cause); + } + + public MetaClientInterruptException(String message) { + super(message); + } + + public MetaClientInterruptException(Throwable cause) { + super(cause); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientNoNodeException.java b/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientNoNodeException.java new file mode 100644 index 0000000000..09505fc19d --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientNoNodeException.java @@ -0,0 +1,39 @@ +package org.apache.helix.metaclient.exception; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +public final class MetaClientNoNodeException extends MetaClientException { + public MetaClientNoNodeException() { + super(); + } + + public MetaClientNoNodeException(String message, Throwable cause) { + super(message, cause); + } + + public MetaClientNoNodeException(String message) { + super(message); + } + + public MetaClientNoNodeException(Throwable cause) { + super(cause); + } + +} \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientTimeoutException.java b/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientTimeoutException.java new file mode 100644 index 0000000000..84b3e97ed1 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/exception/MetaClientTimeoutException.java @@ -0,0 +1,39 @@ +package org.apache.helix.metaclient.exception; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +public final class MetaClientTimeoutException extends MetaClientException { + public MetaClientTimeoutException() { + super(); + } + + public MetaClientTimeoutException(String message, Throwable cause) { + super(message, cause); + } + + public MetaClientTimeoutException(String message) { + super(message); + } + + public MetaClientTimeoutException(Throwable cause) { + super(cause); + } + +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/factories/MetaClientConfig.java b/meta-client/src/main/java/org/apache/helix/metaclient/factories/MetaClientConfig.java new file mode 100644 index 0000000000..501669c7e9 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/factories/MetaClientConfig.java @@ -0,0 +1,174 @@ +package org.apache.helix.metaclient.factories; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.constants.MetaClientConstants; +import org.apache.helix.metaclient.policy.ExponentialBackoffReconnectPolicy; +import org.apache.helix.metaclient.policy.MetaClientReconnectPolicy; + +public class MetaClientConfig { + + public enum StoreType { + ZOOKEEPER, ETCD, CUSTOMIZED + } + + private final String _connectionAddress; + + // Wait for init timeout time until connection is initiated + private final long _connectionInitTimeoutInMillis; + + // When a client becomes partitioned from the metadata service for more than session timeout, + // new session will be established when reconnect. + private final long _sessionTimeoutInMillis; + + // Policy to define client re-establish connection behavior when the connection to underlying + // metadata store is expired. + private final MetaClientReconnectPolicy _metaClientReconnectPolicy; + + private final boolean _enableAuth; + private final StoreType _storeType; + + public String getConnectionAddress() { + return _connectionAddress; + } + + public long getConnectionInitTimeoutInMillis() { + return _connectionInitTimeoutInMillis; + } + + public boolean isAuthEnabled() { + return _enableAuth; + } + + public StoreType getStoreType() { + return _storeType; + } + + public long getSessionTimeoutInMillis() { + return _sessionTimeoutInMillis; + } + + public MetaClientReconnectPolicy getMetaClientReconnectPolicy() { + return _metaClientReconnectPolicy; + } + + // TODO: More options to add later + // private boolean _autoReRegistWatcher; // re-register one time watcher when set to true + // private boolean _resetWatchWhenReConnect; // re-register previous existing watcher when reconnect + + protected MetaClientConfig(String connectionAddress, long connectionInitTimeoutInMillis, + long sessionTimeoutInMillis, MetaClientReconnectPolicy metaClientReconnectPolicy, + boolean enableAuth, StoreType storeType) { + _connectionAddress = connectionAddress; + _connectionInitTimeoutInMillis = connectionInitTimeoutInMillis; + _sessionTimeoutInMillis = sessionTimeoutInMillis; + _metaClientReconnectPolicy = metaClientReconnectPolicy; + _enableAuth = enableAuth; + _storeType = storeType; + } + + public static class MetaClientConfigBuilder> { + protected String _connectionAddress; + + protected long _connectionInitTimeoutInMillis; + protected long _sessionTimeoutInMillis; + protected boolean _enableAuth; + protected StoreType _storeType; + protected MetaClientReconnectPolicy _metaClientReconnectPolicy; + + + public MetaClientConfig build() { + validate(); + return new MetaClientConfig(_connectionAddress, _connectionInitTimeoutInMillis, + _sessionTimeoutInMillis, _metaClientReconnectPolicy, _enableAuth, _storeType); + } + + public MetaClientConfigBuilder() { + // set default values + setStoreType(StoreType.ZOOKEEPER); + setAuthEnabled(false); + setConnectionInitTimeoutInMillis(MetaClientConstants.DEFAULT_CONNECTION_INIT_TIMEOUT_MS); + setSessionTimeoutInMillis(MetaClientConstants.DEFAULT_SESSION_TIMEOUT_MS); + } + + public B setConnectionAddress(String connectionAddress) { + _connectionAddress = connectionAddress; + return self(); + } + + public B setAuthEnabled(Boolean enableAuth) { + _enableAuth = enableAuth; + return self(); + } + + /** + * Set timeout in ms for connection initialization timeout + * @param timeout + * @return + */ + public B setConnectionInitTimeoutInMillis(long timeout) { + _connectionInitTimeoutInMillis = timeout; + return self(); + } + + /** + * Set reconnect policy when connection is lost or expired. By default is + * ExponentialBackoffReconnectPolicy + * @param reconnectPolicy an instance of type MetaClientReconnectPolicy + * @return + */ + public B setMetaClientReconnectPolicy(MetaClientReconnectPolicy reconnectPolicy) { + _metaClientReconnectPolicy = reconnectPolicy; + return self(); + } + + /** + * Set timeout in mm for session timeout. When a client becomes partitioned from the metadata + * service for more than session timeout, new session will be established. + * @param timeout + * @return + */ + public B setSessionTimeoutInMillis(long timeout) { + _sessionTimeoutInMillis = timeout; + return self(); + } + + public B setStoreType(StoreType storeType) { + _storeType = storeType; + return self(); + } + + @SuppressWarnings("unchecked") + final B self() { + return (B) this; + } + + protected void validate() { + if (_metaClientReconnectPolicy == null) { + _metaClientReconnectPolicy = new ExponentialBackoffReconnectPolicy(); + } + + if (_storeType == null || _connectionAddress == null) { + throw new IllegalArgumentException( + "MetaClientConfig.Builder: store type or connection string is null"); + } + } + } +} \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/factories/MetaClientFactory.java b/meta-client/src/main/java/org/apache/helix/metaclient/factories/MetaClientFactory.java new file mode 100644 index 0000000000..0453743326 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/factories/MetaClientFactory.java @@ -0,0 +1,52 @@ +package org.apache.helix.metaclient.factories; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.impl.zk.factory.ZkMetaClientConfig; +import org.apache.helix.metaclient.impl.zk.factory.ZkMetaClientFactory; +import org.apache.helix.zookeeper.datamodel.serializer.ZNRecordSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * A factory class for MetaClient. It returns MetaClient entity based on config. + */ +public class MetaClientFactory { + private static final Logger LOG = LoggerFactory.getLogger(MetaClientFactory.class); + + public MetaClientInterface getMetaClient(MetaClientConfig config) { + if (config == null) { + throw new IllegalArgumentException("MetaClientConfig cannot be null."); + } + if (MetaClientConfig.StoreType.ZOOKEEPER.equals(config.getStoreType())) { + ZkMetaClientConfig zkMetaClientConfig = new ZkMetaClientConfig.ZkMetaClientConfigBuilder(). + setConnectionAddress(config.getConnectionAddress()) + .setMetaClientReconnectPolicy(config.getMetaClientReconnectPolicy()) + .setConnectionInitTimeoutInMillis(config.getConnectionInitTimeoutInMillis()) + .setSessionTimeoutInMillis(config.getSessionTimeoutInMillis()) + .build(); + return new ZkMetaClientFactory().getMetaClient(zkMetaClientConfig); + } + return null; + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/ZkMetaClient.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/ZkMetaClient.java new file mode 100644 index 0000000000..4398e8e3ad --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/ZkMetaClient.java @@ -0,0 +1,557 @@ +package org.apache.helix.metaclient.impl.zk; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.helix.metaclient.api.AsyncCallback; +import org.apache.helix.metaclient.api.ChildChangeListener; +import org.apache.helix.metaclient.api.ConnectStateChangeListener; +import org.apache.helix.metaclient.api.DataChangeListener; +import org.apache.helix.metaclient.api.DataUpdater; +import org.apache.helix.metaclient.api.DirectChildChangeListener; +import org.apache.helix.metaclient.api.DirectChildSubscribeResult; +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.api.Op; +import org.apache.helix.metaclient.api.OpResult; +import org.apache.helix.metaclient.exception.MetaClientException; +import org.apache.helix.metaclient.exception.MetaClientNoNodeException; +import org.apache.helix.metaclient.impl.zk.adapter.ChildListenerAdapter; +import org.apache.helix.metaclient.impl.zk.adapter.DataListenerAdapter; +import org.apache.helix.metaclient.impl.zk.adapter.DirectChildListenerAdapter; +import org.apache.helix.metaclient.impl.zk.adapter.StateChangeListenerAdapter; +import org.apache.helix.metaclient.impl.zk.adapter.ZkMetaClientCreateCallbackHandler; +import org.apache.helix.metaclient.impl.zk.adapter.ZkMetaClientDeleteCallbackHandler; +import org.apache.helix.metaclient.impl.zk.adapter.ZkMetaClientExistCallbackHandler; +import org.apache.helix.metaclient.impl.zk.adapter.ZkMetaClientGetCallbackHandler; +import org.apache.helix.metaclient.impl.zk.adapter.ZkMetaClientSetCallbackHandler; +import org.apache.helix.metaclient.impl.zk.factory.ZkMetaClientConfig; +import org.apache.helix.metaclient.impl.zk.util.ZkMetaClientUtil; +import org.apache.helix.zookeeper.api.client.ChildrenSubscribeResult; +import org.apache.helix.zookeeper.impl.client.ZkClient; +import org.apache.helix.zookeeper.zkclient.IZkStateListener; +import org.apache.helix.zookeeper.zkclient.ZkConnection; +import org.apache.helix.zookeeper.zkclient.exception.ZkException; +import org.apache.helix.zookeeper.zkclient.exception.ZkInterruptedException; +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.Watcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.helix.metaclient.impl.zk.util.ZkMetaClientUtil.convertZkEntryModeToMetaClientEntryMode; +import static org.apache.helix.metaclient.impl.zk.util.ZkMetaClientUtil.translateZkExceptionToMetaclientException; + + +public class ZkMetaClient implements MetaClientInterface, AutoCloseable { + private static final Logger LOG = LoggerFactory.getLogger(ZkMetaClient.class); + private final ZkClient _zkClient; + private final long _initConnectionTimeout; + private final long _reconnectTimeout; + + // After ZkClient gets disconnected from ZK server, it keeps retrying connection until connection + // is re-established or ZkClient is closed. We need a separate thread to monitor ZkClient + // reconnect and close ZkClient if it not able to reconnect within user specified timeout. + private final ScheduledExecutorService _zkClientReconnectMonitor; + private ScheduledFuture _reconnectMonitorFuture; + private ReconnectStateChangeListener _reconnectStateChangeListener; + // Lock all activities related to ZkClient connection + private ReentrantLock _zkClientConnectionMutex = new ReentrantLock(); + + + public ZkMetaClient(ZkMetaClientConfig config) { + _initConnectionTimeout = config.getConnectionInitTimeoutInMillis(); + _reconnectTimeout = config.getMetaClientReconnectPolicy().getAutoReconnectTimeout(); + // TODO: Right new ZkClient reconnect using exp backoff with fixed max backoff interval. We should + // Allow user to config reconnect policy + _zkClient = new ZkClient( + new ZkConnection(config.getConnectionAddress(), (int) config.getSessionTimeoutInMillis()), + (int) _initConnectionTimeout, _reconnectTimeout /*use reconnect timeout for retry timeout*/, + config.getZkSerializer(), config.getMonitorType(), config.getMonitorKey(), + config.getMonitorInstanceName(), config.getMonitorRootPathOnly(), false, true); + _zkClientReconnectMonitor = Executors.newSingleThreadScheduledExecutor(); + _reconnectStateChangeListener = new ReconnectStateChangeListener(); + } + + @Override + public void create(String key, Object data) { + try { + create(key, data, EntryMode.PERSISTENT); + } catch (Exception e) { + throw new MetaClientException(e); + } + } + + @Override + public void create(String key, Object data, MetaClientInterface.EntryMode mode) { + + try{ + _zkClient.create(key, data, ZkMetaClientUtil.convertMetaClientMode(mode)); + } catch (ZkException | KeeperException e) { + throw new MetaClientException(e); + } + } + + @Override + public void createWithTTL(String key, T data, long ttl) { + try{ + _zkClient.createPersistentWithTTL(key, data, ttl); + } catch (ZkException e) { + throw translateZkExceptionToMetaclientException(e); + } + } + + @Override + public void renewTTLNode(String key) { + T oldData = get(key); + if (oldData == null) { + throw new MetaClientNoNodeException("Node at " + key + " does not exist."); + } + set(key, oldData, _zkClient.getStat(key).getVersion()); + } + + @Override + public void set(String key, T data, int version) { + try { + _zkClient.writeData(key, data, version); + } catch (ZkException e) { + throw translateZkExceptionToMetaclientException(e); + } + } + + @Override + public T update(String key, DataUpdater updater) { + org.apache.zookeeper.data.Stat stat = new org.apache.zookeeper.data.Stat(); + // TODO: add retry logic for ZkBadVersionException. + try { + T oldData = _zkClient.readData(key, stat); + T newData = updater.update(oldData); + set(key, newData, stat.getVersion()); + return newData; + } catch (ZkException e) { + throw translateZkExceptionToMetaclientException(e); + } + } + + //TODO: Get Expiry Time in Stat + @Override + public Stat exists(String key) { + org.apache.zookeeper.data.Stat zkStats; + try { + zkStats = _zkClient.getStat(key); + if (zkStats == null) { + return null; + } + return new Stat(convertZkEntryModeToMetaClientEntryMode(zkStats.getEphemeralOwner()), + zkStats.getVersion(), zkStats.getCtime(), zkStats.getMtime(), -1); + } catch (ZkException e) { + throw translateZkExceptionToMetaclientException(e); + } + } + + @Override + public T get(String key) { + return _zkClient.readData(key, true); + } + + @Override + public List getDirectChildrenKeys(String key) { + try { + return _zkClient.getChildren(key); + } catch (ZkException e) { + throw translateZkExceptionToMetaclientException(e); + } + } + + @Override + public int countDirectChildren(String key) { + return _zkClient.countChildren(key); + } + + @Override + public boolean delete(String key) { + try { + return _zkClient.delete(key); + } catch (ZkException e) { + throw translateZkExceptionToMetaclientException(e); + } + } + + @Override + public boolean recursiveDelete(String key) { + _zkClient.deleteRecursively(key); + return true; + } + + // In Current ZkClient, Async CRUD do auto retry when connection lost or session mismatch using + // existing retry handling logic in zkClient. (defined in ZkAsyncCallbacks) + // ZkClient execute async callbacks at zkClient main thead, retry is handles in a separate retry + // thread. In our first version of implementation, we will keep similar behavior and have + // callbacks executed in ZkClient event thread, and reuse zkClient retry logic. + + // It is highly recommended *NOT* to perform any blocking operation inside the callbacks. + // If you block the thread the meta client won't process other events. + + // corresponding callbacks for each operation are invoked in order. + @Override + public void setAsyncExecPoolSize(int poolSize) { + throw new UnsupportedOperationException( + "All async calls are executed in a single thread to maintain sequence."); + } + + @Override + public void asyncCreate(String key, Object data, EntryMode mode, AsyncCallback.VoidCallback cb) { + CreateMode entryMode; + try { + entryMode = ZkMetaClientUtil.convertMetaClientMode(mode); + } catch (ZkException | KeeperException e) { + throw new MetaClientException(e); + } + _zkClient.asyncCreate(key, data, entryMode, + new ZkMetaClientCreateCallbackHandler(cb)); + } + + @Override + public void asyncUpdate(String key, DataUpdater updater, AsyncCallback.DataCallback cb) { + throw new NotImplementedException("Currently asyncUpdate is not supported in ZkMetaClient."); + /* + * TODO: Only Helix has potential using this API as of now. (ZkBaseDataAccessor.update()) + * Will move impl from ZkBaseDataAccessor to here when retiring ZkBaseDataAccessor. + */ + } + + @Override + public void asyncGet(String key, AsyncCallback.DataCallback cb) { + _zkClient.asyncGetData(key, + new ZkMetaClientGetCallbackHandler(cb)); + } + + @Override + public void asyncCountChildren(String key, AsyncCallback.DataCallback cb) { + throw new NotImplementedException( + "Currently asyncCountChildren is not supported in ZkMetaClient."); + /* + * TODO: Only Helix has potential using this API as of now. (ZkBaseDataAccessor.getChildren()) + * Will move impl from ZkBaseDataAccessor to here when retiring ZkBaseDataAccessor. + */ + + } + + @Override + public void asyncExist(String key, AsyncCallback.StatCallback cb) { + _zkClient.asyncExists(key, + new ZkMetaClientExistCallbackHandler(cb)); + } + + public void asyncDelete(String key, AsyncCallback.VoidCallback cb) { + _zkClient.asyncDelete(key, new ZkMetaClientDeleteCallbackHandler(cb)); + } + + @Override + public void asyncTransaction(Iterable ops, AsyncCallback.TransactionCallback cb) { + throw new NotImplementedException( + "Currently asyncTransaction is not supported in ZkMetaClient."); + + //TODO: There is no active use case for Async transaction. + } + + @Override + public void asyncSet(String key, T data, int version, AsyncCallback.StatCallback cb) { + _zkClient.asyncSetData(key, data, version, + new ZkMetaClientSetCallbackHandler(cb)); + } + + @Override + public void connect() { + try { + _zkClientConnectionMutex.lock(); + _zkClient.connect(_initConnectionTimeout, _zkClient); + // register _reconnectStateChangeListener as state change listener to react to ZkClient connect + // state change event. When ZkClient disconnected from ZK, it still auto reconnect until + // ZkClient is closed or connection re-established. + // We will need to close ZkClient when user set retry connection timeout. + _zkClient.subscribeStateChanges(_reconnectStateChangeListener); + } catch (ZkException e) { + throw translateZkExceptionToMetaclientException(e); + } finally { + _zkClientConnectionMutex.unlock(); + } + } + + @Override + public void disconnect() { + cleanUpAndClose(true, true); + _zkClientReconnectMonitor.shutdownNow(); + } + + @Override + public ConnectState getClientConnectionState() { + return null; + } + + @Override + public boolean subscribeDataChange(String key, DataChangeListener listener, boolean skipWatchingNonExistNode) { + _zkClient.subscribeDataChanges(key, new DataListenerAdapter(listener)); + return true; + } + + @Override + public DirectChildSubscribeResult subscribeDirectChildChange(String key, + DirectChildChangeListener listener, boolean skipWatchingNonExistNode) { + ChildrenSubscribeResult result = + _zkClient.subscribeChildChanges(key, new DirectChildListenerAdapter(listener), skipWatchingNonExistNode); + return new DirectChildSubscribeResult(result.getChildren(), result.isInstalled()); + } + + @Override + public boolean subscribeStateChanges(ConnectStateChangeListener listener) { + _zkClient.subscribeStateChanges(new StateChangeListenerAdapter(listener)); + return true; + } + + @Override + public boolean subscribeChildChanges(String key, ChildChangeListener listener, boolean skipWatchingNonExistNode) { + if (skipWatchingNonExistNode && exists(key) == null) { + return false; + } + _zkClient.subscribePersistRecursiveListener(key, new ChildListenerAdapter(listener)); + return true; + } + + @Override + public void unsubscribeDataChange(String key, DataChangeListener listener) { + _zkClient.unsubscribeDataChanges(key, new DataListenerAdapter(listener)); + } + + @Override + public void unsubscribeDirectChildChange(String key, DirectChildChangeListener listener) { + _zkClient.unsubscribeChildChanges(key, new DirectChildListenerAdapter(listener)); + } + + // TODO: add impl and remove UnimplementedException + @Override + public void unsubscribeChildChanges(String key, ChildChangeListener listener) { + _zkClient.unsubscribePersistRecursiveListener(key, new ChildListenerAdapter(listener)); + } + + @Override + public void unsubscribeConnectStateChanges(ConnectStateChangeListener listener) { + _zkClient.subscribeStateChanges(new StateChangeListenerAdapter(listener)); + } + + @Override + public boolean waitUntilExists(String key, TimeUnit timeUnit, long time) { + return false; + } + + @Override + public boolean[] create(List key, List data, List mode) { + return new boolean[0]; + } + + @Override + public boolean[] create(List key, List data) { + return new boolean[0]; + } + + @Override + public boolean[] delete(List keys) { + return new boolean[0]; + } + + @Override + public List exists(List keys) { + return null; + } + + @Override + public List get(List keys) { + return null; + } + + @Override + public List update(List keys, List> updater) { + return null; + } + + @Override + public boolean[] set(List keys, List datas, List version) { + return new boolean[0]; + } + + @Override + public void close() { + disconnect(); + } + + @Override + public List transactionOP(Iterable ops) { + // Convert list of MetaClient Ops to Zk Ops + List zkOps = ZkMetaClientUtil.metaClientOpsToZkOps(ops); + // Execute Zk transactional support + List zkResult = _zkClient.multi(zkOps); + // Convert list of Zk OpResults to MetaClient OpResults + return ZkMetaClientUtil.zkOpResultToMetaClientOpResults(zkResult); + } + + @Override + public byte[] serialize(T data, String path) { + return _zkClient.serialize(data, path); + } + + @Override + public T deserialize(byte[] bytes, String path) { + return _zkClient.deserialize(bytes, path); + } + + /** + * A clean up method called when connect state change or MetaClient is closing. + * @param cancel If we want to cancel the reconnect monitor thread. + * @param close If we want to close ZkClient. + */ + private void cleanUpAndClose(boolean cancel, boolean close) { + _zkClientConnectionMutex.lock(); + try { + if (close && !_zkClient.isClosed()) { + _zkClient.close(); + // TODO: need to unsubscribe all persist watcher from ZK + // Add this in ZkClient when persist watcher change is in + // Also need to manually send CLOSED state change to state + // change listener (in change adapter) + LOG.info("ZkClient is closed"); + } + + if (cancel && _reconnectMonitorFuture != null) { + _reconnectMonitorFuture.cancel(true); + LOG.info("ZkClient reconnect monitor thread is canceled"); + } + + } finally { + _zkClientConnectionMutex.unlock(); + } + } + + /** + * MetaClient uses Helix ZkClient (@see org.apache.helix.zookeeper.impl.client.ZkClient) to connect + * to ZK. Current implementation of ZkClient auto-reconnects infinitely. We use monitor thread + * in ZkMetaClient to monitor reconnect status and close ZkClient when the client still is in + * disconnected state when it reach reconnect timeout. + * + * + * case 1: Start the monitor thread when ZkMetaClient gets disconnected even to check connect state + * when timeout reached. If not re-connected when timed out, kill the monitor thread + * and close ZkClient. + * [MetaClient thread] --------------------------------------------------------------- + * ( When disconnected, schedule a event + * to check connect state after timeout) + * [Reconnect monitor thread] -------------------------------------- + * ^ | not reconnected when timed out + * / | + * | disconnected event v + * [ZkClient] -------X---------------------------------------X zkClient.close() + * [ZkClient exp back | X X + * -off retry connection] |--------|--------------|-------------- + * + * + * case 2: Start the monitor thread when ZkMetaClient gets disconnected even to check connect state + * when timeout reached. If re-connected before timed out, cancel the delayed monitor thread. + * + * [MetaClient thread] --------------------------------------------------------------- + * (cancel scheduled task when reconnected) + * [Reconnect monitor] ---------------------------------X + * ^ ^ + * / / + * | disconnected event | reconnected event + * [ZkClient] -----X------------------------------------------------------ + * [ZkClient exp back | X Y Reconnected before timed out + * -off retry connection] |--------| ---------------------| + * + * + * case 3: Start the monitor thread when ZkMetaClient gets disconnected even to check connect state + * when timeout reached. If re-connected errored, kill the monitor thread and cancel the + * delayed monitor thread. + * [MetaClient thread] --------------------------------------------------------------- + * (cancel scheduled task and close ZkClient when reconnected error) + * [Reconnect monitor] ----------------------------------X + * ^ ^ | + * / err / | + * | disconnected event | v close ZkClient + * [ZkClient] -----X-------------------------------X ---X + * [ZkClient exp back | X ^ Reconnect error + * -off retry connection] |--------| --------------------| + * + */ + + private class ReconnectStateChangeListener implements IZkStateListener { + // Schedule a monitor to track ZkClient auto reconnect when Disconnected + // Cancel the monitor thread when connected. + @Override + public void handleStateChanged(Watcher.Event.KeeperState state) throws Exception { + if (state == Watcher.Event.KeeperState.Disconnected) { // ------case 1 + // Expired. start a new event monitoring retry + _zkClientConnectionMutex.lockInterruptibly(); + try { + if (_reconnectMonitorFuture == null || _reconnectMonitorFuture.isCancelled() + || _reconnectMonitorFuture.isDone()) { + _reconnectMonitorFuture = _zkClientReconnectMonitor.schedule(() -> { + if (!_zkClient.getConnection().getZookeeperState().isConnected()) { + cleanUpAndClose(false, true); + } + }, _reconnectTimeout, TimeUnit.MILLISECONDS); + LOG.info("ZkClient is Disconnected, schedule a reconnect monitor after {}", + _reconnectTimeout); + } + } finally { + _zkClientConnectionMutex.unlock(); + } + } else if (state == Watcher.Event.KeeperState.SyncConnected + || state == Watcher.Event.KeeperState.ConnectedReadOnly) { // ------ case 2 + cleanUpAndClose(true, false); + LOG.info("ZkClient is SyncConnected, reconnect monitor thread is canceled (if any)"); + } + } + + // Cancel the monitor thread when connected. + @Override + public void handleNewSession(String sessionId) throws Exception { // ------ case 2 + cleanUpAndClose(true, false); + LOG.info("New session initiated in ZkClient, reconnect monitor thread is canceled (if any)"); + } + + // Cancel the monitor thread and close ZkClient when connect error. + @Override + public void handleSessionEstablishmentError(Throwable error) throws Exception { // -- case 3 + cleanUpAndClose(true, true); + LOG.info("New session initiated in ZkClient, reconnect monitor thread is canceled (if any)"); + } + } + + @VisibleForTesting + ZkClient getZkClient() { + return _zkClient; + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ChildListenerAdapter.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ChildListenerAdapter.java new file mode 100644 index 0000000000..19c1d64d34 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ChildListenerAdapter.java @@ -0,0 +1,70 @@ +package org.apache.helix.metaclient.impl.zk.adapter; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.List; +import org.apache.helix.metaclient.api.ChildChangeListener; +import org.apache.helix.zookeeper.zkclient.IZkChildListener; +import org.apache.helix.zookeeper.zkclient.RecursivePersistListener; +import org.apache.zookeeper.Watcher; + + +/** + * A adapter class to transform {@link ChildChangeListener} to {@link IZkChildListener}. + */ +public class ChildListenerAdapter implements RecursivePersistListener { + private final ChildChangeListener _listener; + + public ChildListenerAdapter(ChildChangeListener listener) { + _listener = listener; + } + + private static ChildChangeListener.ChangeType convertType(Watcher.Event.EventType eventType) { + switch (eventType) { + case NodeCreated: return ChildChangeListener.ChangeType.ENTRY_CREATED; + case NodeDataChanged: return ChildChangeListener.ChangeType.ENTRY_DATA_CHANGE; + case NodeDeleted: return ChildChangeListener.ChangeType.ENTRY_DELETED; + default: throw new IllegalArgumentException("EventType " + eventType + " is not supported."); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChildListenerAdapter that = (ChildListenerAdapter) o; + return _listener.equals(that._listener); + } + + @Override + public int hashCode() { + return _listener.hashCode(); + } + + @Override + public void handleZNodeChange(String dataPath, Watcher.Event.EventType eventType) + throws Exception { + _listener.handleChildChange(dataPath, convertType(eventType)); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/DataListenerAdapter.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/DataListenerAdapter.java new file mode 100644 index 0000000000..94ae198ce1 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/DataListenerAdapter.java @@ -0,0 +1,77 @@ +package org.apache.helix.metaclient.impl.zk.adapter; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.api.DataChangeListener; +import org.apache.helix.zookeeper.zkclient.IZkDataListener; +import org.apache.zookeeper.Watcher; + + +/** + * A Adapter class to transform {@link DataChangeListener} to {@link IZkDataListener} + */ +public class DataListenerAdapter implements IZkDataListener { + private final DataChangeListener _listener; + + public DataListenerAdapter(DataChangeListener listener) { + _listener = listener; + } + + @Override + public void handleDataChange(String dataPath, Object data) throws Exception { + throw new UnsupportedOperationException("handleDataChange(String dataPath, Object data) is not supported."); + } + + @Override + public void handleDataDeleted(String dataPath) throws Exception { + handleDataChange(dataPath, null, Watcher.Event.EventType.NodeDeleted); + } + + @Override + public void handleDataChange(String dataPath, Object data, Watcher.Event.EventType eventType) throws Exception { + _listener.handleDataChange(dataPath, data, convertType(eventType)); + } + + private static DataChangeListener.ChangeType convertType(Watcher.Event.EventType eventType) { + switch (eventType) { + case NodeCreated: return DataChangeListener.ChangeType.ENTRY_CREATED; + case NodeDataChanged: return DataChangeListener.ChangeType.ENTRY_UPDATE; + case NodeDeleted: return DataChangeListener.ChangeType.ENTRY_DELETED; + default: throw new IllegalArgumentException("EventType " + eventType + " is not supported."); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DataListenerAdapter that = (DataListenerAdapter) o; + return _listener.equals(that._listener); + } + + @Override + public int hashCode() { + return _listener.hashCode(); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/DirectChildListenerAdapter.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/DirectChildListenerAdapter.java new file mode 100644 index 0000000000..b65bf5de1c --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/DirectChildListenerAdapter.java @@ -0,0 +1,55 @@ +package org.apache.helix.metaclient.impl.zk.adapter; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.List; +import org.apache.helix.metaclient.api.DirectChildChangeListener; +import org.apache.helix.zookeeper.zkclient.IZkChildListener; + + +public class DirectChildListenerAdapter implements IZkChildListener { + private final DirectChildChangeListener _listener; + + public DirectChildListenerAdapter(DirectChildChangeListener listener) { + _listener = listener; + } + + @Override + public void handleChildChange(String parentPath, List currentChildren) throws Exception { + _listener.handleDirectChildChange(parentPath); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DirectChildListenerAdapter that = (DirectChildListenerAdapter) o; + return _listener.equals(that._listener); + } + + @Override + public int hashCode() { + return _listener.hashCode(); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/StateChangeListenerAdapter.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/StateChangeListenerAdapter.java new file mode 100644 index 0000000000..8ad324bb27 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/StateChangeListenerAdapter.java @@ -0,0 +1,76 @@ +package org.apache.helix.metaclient.impl.zk.adapter; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.api.ConnectStateChangeListener; +import org.apache.helix.metaclient.impl.zk.util.ZkMetaClientUtil; +import org.apache.helix.zookeeper.zkclient.IZkStateListener; +import org.apache.zookeeper.Watcher; + + +public class StateChangeListenerAdapter implements IZkStateListener { + private final ConnectStateChangeListener _listener; + + public StateChangeListenerAdapter(ConnectStateChangeListener listener) { + _listener = listener; + } + + @Override + public void handleStateChanged(Watcher.Event.KeeperState state) throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + public void handleNewSession(String sessionId) throws Exception { + // This function will be invoked when connection is established. It is a no-op for metaclient. + // MetaClient will expose this to user as 'handleStateChanged' already covers state change + // notification for new connection establishment. + } + + @Override + public void handleSessionEstablishmentError(Throwable error) throws Exception { + _listener.handleConnectionEstablishmentError(error); + } + + @Override + public void handleStateChanged(Watcher.Event.KeeperState prevState, + Watcher.Event.KeeperState curState) throws Exception { + _listener.handleConnectStateChanged( + ZkMetaClientUtil.translateKeeperStateToMetaClientConnectState(prevState), + ZkMetaClientUtil.translateKeeperStateToMetaClientConnectState(curState)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StateChangeListenerAdapter that = (StateChangeListenerAdapter) o; + return _listener.equals(that._listener); + } + + @Override + public int hashCode() { + return _listener.hashCode(); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientCreateCallbackHandler.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientCreateCallbackHandler.java new file mode 100644 index 0000000000..358b447db7 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientCreateCallbackHandler.java @@ -0,0 +1,48 @@ +package org.apache.helix.metaclient.impl.zk.adapter; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +import org.apache.helix.metaclient.api.AsyncCallback; +import org.apache.helix.zookeeper.zkclient.callback.ZkAsyncCallbacks; + +/** + * Wrapper class for metaclient.api.AsyncCallback. + * This wrapper class extends zk callback class. It has an object of user defined + * metaclient.api.AsyncCallback. + * Each callback will do default retry defined in ZkAsyncCallbacks. (defined in ZkAsyncCallbacks) + * + * ZkClient execute async callbacks at zkClient main thead, retry is handles in a separate retry + * thread. In our first version of implementation, we will keep similar behavior and have + * callbacks executed in ZkClient event thread, and reuse zkclient retry logic. + */ + +public class ZkMetaClientCreateCallbackHandler extends ZkAsyncCallbacks.CreateCallbackHandler { + AsyncCallback.VoidCallback _userCallback; + + public ZkMetaClientCreateCallbackHandler(AsyncCallback.VoidCallback cb) { + _userCallback = cb; + } + + @Override + public void handle() { + _userCallback.processResult(getRc(), getPath()); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientDeleteCallbackHandler.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientDeleteCallbackHandler.java new file mode 100644 index 0000000000..59f173f818 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientDeleteCallbackHandler.java @@ -0,0 +1,47 @@ +package org.apache.helix.metaclient.impl.zk.adapter; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.api.AsyncCallback; +import org.apache.helix.zookeeper.zkclient.callback.ZkAsyncCallbacks; + +/** + * Wrapper class for metaclient.api.AsyncCallback. + * This wrapper class extends zk callback class. It has an object of user defined + * metaclient.api.AsyncCallback. + * Each callback will do default retry defined in ZkAsyncCallbacks. (defined in ZkAsyncCallbacks) + * + * ZkClient execute async callbacks at zkClient main thead, retry is handles in a separate retry + * thread. In our first version of implementation, we will keep similar behavior and have + * callbacks executed in ZkClient event thread, and reuse zkclient retry logic. + */ + +public class ZkMetaClientDeleteCallbackHandler extends ZkAsyncCallbacks.DeleteCallbackHandler { + AsyncCallback.VoidCallback _userCallback; + + public ZkMetaClientDeleteCallbackHandler(AsyncCallback.VoidCallback cb) { + _userCallback = cb; + } + + @Override + public void handle() { + _userCallback.processResult(getRc(), getPath()); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientExistCallbackHandler.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientExistCallbackHandler.java new file mode 100644 index 0000000000..eefafc339b --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientExistCallbackHandler.java @@ -0,0 +1,53 @@ +package org.apache.helix.metaclient.impl.zk.adapter; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.api.AsyncCallback; +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.impl.zk.util.ZkMetaClientUtil; +import org.apache.helix.zookeeper.zkclient.callback.ZkAsyncCallbacks; + +/** + * Wrapper class for metaclient.api.AsyncCallback. + * This wrapper class extends zk callback class. It has an object of user defined + * metaclient.api.AsyncCallback. + * Each callback will do default retry defined in ZkAsyncCallbacks. (defined in ZkAsyncCallbacks) + * + * ZkClient execute async callbacks at zkClient main thead, retry is handles in a separate retry + * thread. In our first version of implementation, we will keep similar behavior and have + * callbacks executed in ZkClient event thread, and reuse zkclient retry logic. + */ + +public class ZkMetaClientExistCallbackHandler extends ZkAsyncCallbacks.ExistsCallbackHandler { + AsyncCallback.StatCallback _userCallback; + + public ZkMetaClientExistCallbackHandler(AsyncCallback.StatCallback cb) { + _userCallback = cb; + } + + // Call user passed in callback. Will pass a null for stats if operation fails. + @Override + public void handle() { + _userCallback.processResult(getRc(), getPath(), getStat() == null ? null + : new MetaClientInterface.Stat( + ZkMetaClientUtil.convertZkEntryModeToMetaClientEntryMode(getStat().getEphemeralOwner()), + getStat().getVersion())); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientGetCallbackHandler.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientGetCallbackHandler.java new file mode 100644 index 0000000000..08efb2d54b --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientGetCallbackHandler.java @@ -0,0 +1,54 @@ +package org.apache.helix.metaclient.impl.zk.adapter; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.api.AsyncCallback; +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.impl.zk.util.ZkMetaClientUtil; +import org.apache.helix.zookeeper.zkclient.callback.ZkAsyncCallbacks; + +/** + * Wrapper class for metaclient.api.AsyncCallback. + * This wrapper class extends zk callback class. It has an object of user defined + * metaclient.api.AsyncCallback. + * Each callback will do default retry defined in ZkAsyncCallbacks. (defined in ZkAsyncCallbacks) + * + * ZkClient execute async callbacks at zkClient main thead, retry is handles in a separate retry + * thread. In our first version of implementation, we will keep similar behavior and have + * callbacks executed in ZkClient event thread, and reuse zkclient retry logic. + */ + +public class ZkMetaClientGetCallbackHandler extends ZkAsyncCallbacks.GetDataCallbackHandler { + AsyncCallback.DataCallback _userCallback; + + public ZkMetaClientGetCallbackHandler(AsyncCallback.DataCallback cb) { + _userCallback = cb; + } + + + // Call user passed in callback. Will pass a null for stats if get operation fails. + @Override + public void handle() { + _userCallback.processResult(getRc(), getPath(), getData(), + getStat() == null ? null : new MetaClientInterface.Stat( + ZkMetaClientUtil.convertZkEntryModeToMetaClientEntryMode(getStat().getEphemeralOwner()), + getStat().getVersion())); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientSetCallbackHandler.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientSetCallbackHandler.java new file mode 100644 index 0000000000..60eecc7f88 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/adapter/ZkMetaClientSetCallbackHandler.java @@ -0,0 +1,42 @@ +package org.apache.helix.metaclient.impl.zk.adapter; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.api.AsyncCallback; +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.impl.zk.util.ZkMetaClientUtil; +import org.apache.helix.zookeeper.zkclient.callback.ZkAsyncCallbacks; + + +public class ZkMetaClientSetCallbackHandler extends ZkAsyncCallbacks.SetDataCallbackHandler { + AsyncCallback.StatCallback userCallback; + + public ZkMetaClientSetCallbackHandler(AsyncCallback.StatCallback cb) { + userCallback = cb; + } + + @Override + public void handle() { + userCallback.processResult(getRc(), getPath(), getStat() == null ? null + : new MetaClientInterface.Stat( + ZkMetaClientUtil.convertZkEntryModeToMetaClientEntryMode(getStat().getEphemeralOwner()), + getStat().getVersion())); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/factory/ZkMetaClientConfig.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/factory/ZkMetaClientConfig.java new file mode 100644 index 0000000000..63d6ff07cc --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/factory/ZkMetaClientConfig.java @@ -0,0 +1,147 @@ +package org.apache.helix.metaclient.impl.zk.factory; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.factories.MetaClientConfig; +import org.apache.helix.metaclient.policy.MetaClientReconnectPolicy; +import org.apache.helix.zookeeper.zkclient.serialize.BasicZkSerializer; +import org.apache.helix.zookeeper.zkclient.serialize.PathBasedZkSerializer; +import org.apache.helix.zookeeper.zkclient.serialize.SerializableSerializer; +import org.apache.helix.zookeeper.zkclient.serialize.ZkSerializer; + + +public class ZkMetaClientConfig extends MetaClientConfig { + + protected final PathBasedZkSerializer _zkSerializer; + + // Monitoring related fields. MBean names are crated using following variables in format of + // MonitorPrefix_monitorType_monitorKey_monitorInstanceName, where _monitorInstanceName is optional + // TODO: right now all zkClient mBean object has prefix `HelixZkClient` had coded. We should change + // it to a configurable name. + protected final String _monitorType; + protected final String _monitorKey; + protected final String _monitorInstanceName; + protected final boolean _monitorRootPathOnly; + + public PathBasedZkSerializer getZkSerializer() { + return _zkSerializer; + } + + public String getMonitorType() { + return _monitorType; + } + + public String getMonitorKey() { + return _monitorKey; + } + + public String getMonitorInstanceName() { + return _monitorInstanceName; + } + + public boolean getMonitorRootPathOnly() { + return _monitorRootPathOnly; + } + + protected ZkMetaClientConfig(String connectionAddress, long connectionInitTimeoutInMillis, + long sessionTimeoutInMillis, MetaClientReconnectPolicy reconnectPolicy, boolean enableAuth, + StoreType storeType, String monitorType, String monitorKey, String monitorInstanceName, + boolean monitorRootPathOnly, PathBasedZkSerializer zkSerializer) { + super(connectionAddress, connectionInitTimeoutInMillis, sessionTimeoutInMillis, reconnectPolicy, + enableAuth, storeType); + _zkSerializer = zkSerializer; + _monitorType = monitorType; + _monitorKey = monitorKey; + _monitorInstanceName = monitorInstanceName; + _monitorRootPathOnly = monitorRootPathOnly; + } + + public static class ZkMetaClientConfigBuilder extends MetaClientConfig.MetaClientConfigBuilder { + + protected PathBasedZkSerializer _zkSerializer; + + // Monitoring + // Type as in MBean object + protected String _monitorType; + protected String _monitorKey; + protected String _monitorInstanceName = null; + protected boolean _monitorRootPathOnly = true; + + public ZkMetaClientConfigBuilder setZkSerializer( + org.apache.helix.zookeeper.zkclient.serialize.PathBasedZkSerializer zkSerializer) { + this._zkSerializer = zkSerializer; + return this; + } + + public ZkMetaClientConfigBuilder setZkSerializer(ZkSerializer zkSerializer) { + this._zkSerializer = new BasicZkSerializer(zkSerializer); + return this; + } + + /** + * Used as part of the MBean ObjectName. This item is required for enabling monitoring. + * @param monitorType + */ + public ZkMetaClientConfigBuilder setMonitorType(String monitorType) { + this._monitorType = monitorType; + return this; + } + + /** + * Used as part of the MBean ObjectName. This item is required for enabling monitoring. + * @param monitorKey + */ + public ZkMetaClientConfigBuilder setMonitorKey(String monitorKey) { + this._monitorKey = monitorKey; + return this; + } + + /** + * Used as part of the MBean ObjectName. This item is optional. + * @param instanceName + */ + public ZkMetaClientConfigBuilder setMonitorInstanceName(String instanceName) { + this._monitorInstanceName = instanceName; + return this; + } + + public ZkMetaClientConfigBuilder setMonitorRootPathOnly(Boolean monitorRootPathOnly) { + this._monitorRootPathOnly = monitorRootPathOnly; + return this; + } + + @Override + public ZkMetaClientConfig build() { + validate(); + return new ZkMetaClientConfig(_connectionAddress, _connectionInitTimeoutInMillis, + _sessionTimeoutInMillis, _metaClientReconnectPolicy, _enableAuth, + MetaClientConfig.StoreType.ZOOKEEPER, _monitorType, _monitorKey, _monitorInstanceName, + _monitorRootPathOnly, _zkSerializer); + } + + @Override + protected void validate() { + super.validate(); + if (_zkSerializer == null) { + _zkSerializer = new BasicZkSerializer(new SerializableSerializer()); + } + } + } +} \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/factory/ZkMetaClientFactory.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/factory/ZkMetaClientFactory.java new file mode 100644 index 0000000000..9eba28b916 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/factory/ZkMetaClientFactory.java @@ -0,0 +1,39 @@ +package org.apache.helix.metaclient.impl.zk.factory; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.factories.MetaClientConfig; +import org.apache.helix.metaclient.factories.MetaClientFactory; +import org.apache.helix.metaclient.impl.zk.ZkMetaClient; + +public class ZkMetaClientFactory extends MetaClientFactory { + @Override + public MetaClientInterface getMetaClient(MetaClientConfig config) { + if (config == null) { + throw new IllegalArgumentException("MetaClientConfig cannot be null."); + } + if (MetaClientConfig.StoreType.ZOOKEEPER.equals(config.getStoreType()) + && config instanceof ZkMetaClientConfig) { + return new ZkMetaClient((ZkMetaClientConfig) config); + } + throw new IllegalArgumentException("Invalid MetaClientConfig type."); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/util/ZkMetaClientUtil.java b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/util/ZkMetaClientUtil.java new file mode 100644 index 0000000000..f21a883f37 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/impl/zk/util/ZkMetaClientUtil.java @@ -0,0 +1,349 @@ +package org.apache.helix.metaclient.impl.zk.util; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.api.OpResult; +import org.apache.helix.metaclient.exception.MetaClientBadVersionException; +import org.apache.helix.metaclient.exception.MetaClientException; +import org.apache.helix.metaclient.exception.MetaClientInterruptException; +import org.apache.helix.metaclient.exception.MetaClientNoNodeException; +import org.apache.helix.metaclient.exception.MetaClientTimeoutException; +import org.apache.helix.zookeeper.zkclient.exception.ZkBadVersionException; +import org.apache.helix.zookeeper.zkclient.exception.ZkException; +import org.apache.helix.zookeeper.zkclient.exception.ZkInterruptedException; +import org.apache.helix.zookeeper.zkclient.exception.ZkNoNodeException; +import org.apache.helix.zookeeper.zkclient.exception.ZkTimeoutException; +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.Op; +import org.apache.zookeeper.Watcher; +import org.apache.zookeeper.ZooDefs; +import org.apache.zookeeper.data.ACL; +import org.apache.zookeeper.server.EphemeralType; + +public class ZkMetaClientUtil { + //TODO Implement MetaClient ACL + //Default ACL value until metaClient Op has ACL of its own. + private static final List DEFAULT_ACL = + Collections.unmodifiableList(ZooDefs.Ids.OPEN_ACL_UNSAFE); + + private ZkMetaClientUtil() { + } + + /** + * Helper function for transactionOp. Converts MetaClient Op's into Zk Ops to execute + * zk transactional support. + * @param ops + * @return + */ + public static List metaClientOpsToZkOps(Iterable ops) { + List zkOps = new ArrayList<>(); + for (org.apache.helix.metaclient.api.Op op : ops) { + Function function = getOpMap().get(op.getType()); + if (function != null) { + zkOps.add(function.apply(op)); + } else { + throw new IllegalArgumentException("Op type " + op.getType().name() + " is not supported."); + } + } + return zkOps; + } + + private static final class OpMapHolder { + static final Map> OPMAP = initializeOpMap(); + + private static Map> initializeOpMap() { + Map> opmap = + new EnumMap<>(org.apache.helix.metaclient.api.Op.Type.class); + + opmap.put(org.apache.helix.metaclient.api.Op.Type.CREATE, op -> { + try { + CreateMode mode = convertMetaClientMode( + ((org.apache.helix.metaclient.api.Op.Create) op).getEntryMode()); + return Op.create(op.getPath(), ((org.apache.helix.metaclient.api.Op.Create) op).getData(), + DEFAULT_ACL, mode); + } catch (KeeperException e) { + throw translateZkExceptionToMetaclientException(ZkException.create(e)); + } + }); + + opmap.put(org.apache.helix.metaclient.api.Op.Type.DELETE, op -> Op + .delete(op.getPath(), ((org.apache.helix.metaclient.api.Op.Delete) op).getVersion())); + + opmap.put(org.apache.helix.metaclient.api.Op.Type.SET, op -> Op + .setData(op.getPath(), ((org.apache.helix.metaclient.api.Op.Set) op).getData(), + ((org.apache.helix.metaclient.api.Op.Set) op).getVersion())); + + opmap.put(org.apache.helix.metaclient.api.Op.Type.CHECK, op -> Op + .check(op.getPath(), ((org.apache.helix.metaclient.api.Op.Check) op).getVersion())); + + return opmap; + } + } + + private static Map> getOpMap() { + return OpMapHolder.OPMAP; + } + + public static CreateMode convertMetaClientMode(MetaClientInterface.EntryMode entryMode) throws KeeperException { + switch (entryMode) { + case PERSISTENT: + return CreateMode.PERSISTENT; + case EPHEMERAL: + return CreateMode.EPHEMERAL; + case CONTAINER: + return CreateMode.CONTAINER; + default: + throw new IllegalArgumentException(entryMode.name() + " is not a supported EntryMode."); + } + } + + /** + * Helper function for transactionOP. Converts the result from calling zk transactional support into + * metaclient OpResults. + * @param zkResult + * @return + */ + public static List zkOpResultToMetaClientOpResults(List zkResult) { + List metaClientOpResult = new ArrayList<>(); + for (org.apache.zookeeper.OpResult opResult : zkResult) { + Function function = + getOpResultMap().get(opResult.getClass()); + if (function != null) { + metaClientOpResult.add(function.apply(opResult)); + } else { + throw new IllegalArgumentException( + "OpResult type " + opResult.getType() + "is not supported."); + } + } + + return metaClientOpResult; + } + + private static final class OpResultMapHolder { + static final Map, Function> + OPRESULTMAP = initializeOpResultMap(); + + private static Map, Function> initializeOpResultMap() { + Map, Function> + opResultMap = new HashMap<>(); + opResultMap.put(org.apache.zookeeper.OpResult.CreateResult.class, opResult -> { + org.apache.zookeeper.OpResult.CreateResult zkOpCreateResult = + (org.apache.zookeeper.OpResult.CreateResult) opResult; + if (opResult.getType() == 1) { + return new OpResult.CreateResult(zkOpCreateResult.getPath()); + } else { + MetaClientInterface.Stat metaClientStat = new MetaClientInterface.Stat( + convertZkEntryModeToMetaClientEntryMode( + zkOpCreateResult.getStat().getEphemeralOwner()), + zkOpCreateResult.getStat().getVersion()); + return new OpResult.CreateResult(zkOpCreateResult.getPath(), metaClientStat); + } + }); + + opResultMap.put(org.apache.zookeeper.OpResult.DeleteResult.class, + opResult -> new OpResult.DeleteResult()); + + opResultMap.put(org.apache.zookeeper.OpResult.GetDataResult.class, opResult -> { + org.apache.zookeeper.OpResult.GetDataResult zkOpGetDataResult = + (org.apache.zookeeper.OpResult.GetDataResult) opResult; + MetaClientInterface.Stat metaClientStat = new MetaClientInterface.Stat( + convertZkEntryModeToMetaClientEntryMode( + zkOpGetDataResult.getStat().getEphemeralOwner()), + zkOpGetDataResult.getStat().getVersion()); + return new OpResult.GetDataResult(zkOpGetDataResult.getData(), metaClientStat); + }); + + opResultMap.put(org.apache.zookeeper.OpResult.SetDataResult.class, opResult -> { + org.apache.zookeeper.OpResult.SetDataResult zkOpSetDataResult = + (org.apache.zookeeper.OpResult.SetDataResult) opResult; + MetaClientInterface.Stat metaClientStat = new MetaClientInterface.Stat( + convertZkEntryModeToMetaClientEntryMode( + zkOpSetDataResult.getStat().getEphemeralOwner()), + zkOpSetDataResult.getStat().getVersion()); + return new OpResult.SetDataResult(metaClientStat); + }); + + opResultMap.put(org.apache.zookeeper.OpResult.GetChildrenResult.class, + opResult -> new OpResult.GetChildrenResult( + ((org.apache.zookeeper.OpResult.GetChildrenResult) opResult).getChildren())); + + opResultMap.put(org.apache.zookeeper.OpResult.CheckResult.class, + opResult -> new OpResult.CheckResult()); + + opResultMap.put(org.apache.zookeeper.OpResult.ErrorResult.class, + opResult -> new OpResult.ErrorResult( + ((org.apache.zookeeper.OpResult.ErrorResult) opResult).getErr())); + + return opResultMap; + } + } + + private static Map, Function> getOpResultMap() { + return OpResultMapHolder.OPRESULTMAP; + } + + public static MetaClientInterface.EntryMode convertZkEntryModeToMetaClientEntryMode( + long ephemeralOwner) { + EphemeralType zkEphemeralType = EphemeralType.get(ephemeralOwner); + switch (zkEphemeralType) { + case VOID: + return MetaClientInterface.EntryMode.PERSISTENT; + case CONTAINER: + return MetaClientInterface.EntryMode.CONTAINER; + case NORMAL: + return MetaClientInterface.EntryMode.EPHEMERAL; + case TTL: + return MetaClientInterface.EntryMode.TTL; + default: + throw new IllegalArgumentException(zkEphemeralType + " is not supported."); + } + } + + public static MetaClientException translateZkExceptionToMetaclientException(ZkException e) { + if (e instanceof ZkNoNodeException) { + return new MetaClientNoNodeException(e); + } else if (e instanceof ZkBadVersionException) { + return new MetaClientBadVersionException(e); + } else if (e instanceof ZkTimeoutException) { + return new MetaClientTimeoutException(e); + } else if (e instanceof ZkInterruptedException) { + return new MetaClientInterruptException(e); + } + return new MetaClientException(e); + } + + public static MetaClientInterface.ConnectState translateKeeperStateToMetaClientConnectState( + Watcher.Event.KeeperState keeperState) { + if (keeperState == null) + return MetaClientInterface.ConnectState.NOT_CONNECTED; + switch (keeperState) { + case AuthFailed: + return MetaClientInterface.ConnectState.AUTH_FAILED; + case Closed: + return MetaClientInterface.ConnectState.CLOSED_BY_CLIENT; + case Disconnected: + return MetaClientInterface.ConnectState.DISCONNECTED; + case Expired: + return MetaClientInterface.ConnectState.EXPIRED; + case SaslAuthenticated: + return MetaClientInterface.ConnectState.AUTHENTICATED; + case SyncConnected: + case ConnectedReadOnly: + return MetaClientInterface.ConnectState.CONNECTED; + default: + throw new IllegalArgumentException(keeperState + " is not a supported."); + } + } + + /** + * This function translate and group Zk exception code to metaclient code. + * It currently includes all ZK code on 3.6.3. + */ + public static MetaClientException.ReturnCode translateZooKeeperCodeToMetaClientCode( + KeeperException.Code zkCode) { + // TODO: add log to track ZK origional code. + switch (zkCode) { + case AUTHFAILED: + case SESSIONCLOSEDREQUIRESASLAUTH: + case INVALIDACL: + return MetaClientException.ReturnCode.AUTH_FAILED; + + case CONNECTIONLOSS: + return MetaClientException.ReturnCode.CONNECTION_LOSS; + + case BADARGUMENTS: + return MetaClientException.ReturnCode.INVALID_ARGUMENTS; + + case BADVERSION: + return MetaClientException.ReturnCode.BAD_VERSION; + + case NOAUTH: + return MetaClientException.ReturnCode.NO_AUTH; + + case NOWATCHER: + return MetaClientException.ReturnCode.INVALID_LISTENER; + + case NOTEMPTY: + return MetaClientException.ReturnCode.NOT_LEAF_ENTRY; + + case NODEEXISTS: + return MetaClientException.ReturnCode.ENTRY_EXISTS; + + case SESSIONEXPIRED: + case SESSIONMOVED: + case UNKNOWNSESSION: + return MetaClientException.ReturnCode.SESSION_ERROR; + + case NONODE: + return MetaClientException.ReturnCode.NO_SUCH_ENTRY; + + case OPERATIONTIMEOUT: + return MetaClientException.ReturnCode.OPERATION_TIMEOUT; + + case OK: + return MetaClientException.ReturnCode.OK; + + case UNIMPLEMENTED: + return MetaClientException.ReturnCode.UNIMPLEMENTED; + + case RUNTIMEINCONSISTENCY: + case DATAINCONSISTENCY: + return MetaClientException.ReturnCode.CONSISTENCY_ERROR; + + case SYSTEMERROR: + case MARSHALLINGERROR: + case NEWCONFIGNOQUORUM: + case RECONFIGINPROGRESS: + return MetaClientException.ReturnCode.DB_SYSTEM_ERROR; + + case NOCHILDRENFOREPHEMERALS: + case INVALIDCALLBACK: + case NOTREADONLY: + case EPHEMERALONLOCALSESSION: + case RECONFIGDISABLED: + return MetaClientException.ReturnCode.DB_USER_ERROR; + + /* + * APIERROR is ZooKeeper Code value separator. It is never thrown by ZK server, + * ZK error codes greater than its value are user or client errors and values less than + * this indicate a ZK server. + * Note: there are some mismatch between ZK doc and Zk code intValue define. We are comparing + * ordinal instead of using intValue(). + * https://zookeeper.apache.org/doc/r3.6.2/apidocs/zookeeper-server/index.html?org/apache/zookeeper/KeeperException.Code.html + */ + default: + if (zkCode.ordinal() < KeeperException.Code.APIERROR.ordinal() + && zkCode.ordinal() >= KeeperException.Code.SYSTEMERROR.ordinal()) { + return MetaClientException.ReturnCode.DB_SYSTEM_ERROR; + } + return MetaClientException.ReturnCode.DB_USER_ERROR; + } + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/policy/ExponentialBackoffReconnectPolicy.java b/meta-client/src/main/java/org/apache/helix/metaclient/policy/ExponentialBackoffReconnectPolicy.java new file mode 100644 index 0000000000..7c5829e101 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/policy/ExponentialBackoffReconnectPolicy.java @@ -0,0 +1,57 @@ +package org.apache.helix.metaclient.policy; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.policy.MetaClientReconnectPolicy; + +import static org.apache.helix.metaclient.constants.MetaClientConstants.DEFAULT_AUTO_RECONNECT_TIMEOUT_MS; +import static org.apache.helix.metaclient.constants.MetaClientConstants.DEFAULT_INIT_EXP_BACKOFF_RETRY_INTERVAL_MS; +import static org.apache.helix.metaclient.constants.MetaClientConstants.DEFAULT_MAX_EXP_BACKOFF_RETRY_INTERVAL_MS; + +/** + * Policy to define client re-establish connection behavior when connection to underlying metadata + * store is expired. + * Wait time before each backoff period will increase exponentially until a user defined max + * backoff interval. + */ +public class ExponentialBackoffReconnectPolicy implements MetaClientReconnectPolicy { + + private final long _autoReconnectTimeout; + + @Override + public RetryPolicyName getPolicyName() { + return RetryPolicyName.EXP_BACKOFF; + } + + @Override + public long getAutoReconnectTimeout() { + return _autoReconnectTimeout; + } + + public ExponentialBackoffReconnectPolicy() { + _autoReconnectTimeout = DEFAULT_AUTO_RECONNECT_TIMEOUT_MS; + } + + public ExponentialBackoffReconnectPolicy(long autoReconnectTimeout) { + _autoReconnectTimeout = autoReconnectTimeout; + } + + // TODO: Allow user to pass maxBackOffInterval and initBackoffInterval. +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/policy/MetaClientReconnectPolicy.java b/meta-client/src/main/java/org/apache/helix/metaclient/policy/MetaClientReconnectPolicy.java new file mode 100644 index 0000000000..5ef56988e8 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/policy/MetaClientReconnectPolicy.java @@ -0,0 +1,42 @@ +package org.apache.helix.metaclient.policy; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import static org.apache.helix.metaclient.constants.MetaClientConstants.DEFAULT_AUTO_RECONNECT_TIMEOUT_MS; + + +/** + * Policy to define client re-establish connection behavior when connection to underlying metadata + * store is expired. + */ + +public interface MetaClientReconnectPolicy { + + enum RetryPolicyName { + EXP_BACKOFF, + LINEAR_BACKOFF + } + + RetryPolicyName getPolicyName(); + + default long getAutoReconnectTimeout() { + return DEFAULT_AUTO_RECONNECT_TIMEOUT_MS; + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/recipes/leaderelection/LeaderElectionClient.java b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/leaderelection/LeaderElectionClient.java new file mode 100644 index 0000000000..4f609153be --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/leaderelection/LeaderElectionClient.java @@ -0,0 +1,180 @@ +package org.apache.helix.metaclient.recipes.leaderelection; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.List; + +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.factories.MetaClientConfig; + + +/** + * LeaderElectionClient does distributed leader election using CRUD and change notification APIs + * provided by underlying metadata client. Leader election config can provide many + * configs like base path for all participating nodes, sync/async mode, TTL etc. + * + * Participants join a leader election group by calling the following API. + * The Leader Election client maintains and elect an active leader from participant pool. + * All participants wanted to be elected as leader joins a pool. + * LeaderElection client maintains an active leader, by monitoring liveness of current leader and + * re-elect if needed and user no need to call elect or re-elect explicitly. + * This LeaderElection client will notify registered listeners for any leadership change. + * + * One client is created per each participant(host). One participant can join multiple leader + * election groups using the same client. + * When the client is used by a leader election service, one client is created for each participant. + * + */ +public class LeaderElectionClient { + + /** + * Construct a LeaderElectionClient using a user passed in leaderElectionConfig. It creates a MetaClient + * instance underneath. + * When MetaClient is auto closed be cause of being disconnected and auto retry connection timed out, A new + * MetaClient instance will be created and keeps retry connection. + * + * @param metaClientConfig The config used to create an metaclient. + */ + public LeaderElectionClient(MetaClientConfig metaClientConfig, String participant) { + + } + + /** + * Construct a LeaderElectionClient using a user passed in MetaClient object + * When MetaClient is auto closed be cause of being disconnected and auto retry connection timed out, user + * will need to create a new MetaClient and a new LeaderElectionClient instance. + * + * @param metaClient metaClient object to be used. + */ + public LeaderElectionClient(MetaClientInterface metaClient, String participant) { + + } + + /** + * Returns true if current participant is the current leadership. + */ + public boolean isLeader(String leaderPath) { + return false; + } + + /** + * Participants join a leader election group by calling the following API. + * The Leader Election client maintains and elect an active leader from the participant pool. + * + * @param leaderPath The path for leader election. + * @return boolean indicating if the operation is succeeded. + */ + public boolean joinLeaderElectionParticipantPool(String leaderPath) { + return false; + } + + /** + * Participants join a leader election group by calling the following API. + * The Leader Election client maintains and elect an active leader from the participant pool. + * + * @param leaderPath The path for leader election. + * @param userInfo Any additional information to associate with this participant. + * @return boolean indicating if the operation is succeeded. + */ + public boolean joinLeaderElectionParticipantPool(String leaderPath, Object userInfo) { + return false; + } + + /** + * Any participant may exit the exitLeaderElectionParticipantPool by calling the API. + * If the participant is not the current leader, it leaves the pool and won't participant future + * leader election process. + * If the participant is the current leader, it leaves the pool and a new leader will be elected + * if there are other participants in the pool. + * Throws exception if the participant is not in the pool. + * + * @param leaderPath The path for leader election. + * @return boolean indicating if the operation is succeeded. + * + * @throws RuntimeException If the participant did not join participant pool via this client. // TODO: define exp type + */ + public boolean exitLeaderElectionParticipantPool(String leaderPath) { + return false; + } + + /** + * Releases leadership for participant. + * + * @param leaderPath The path for leader election. + * + * @throws RuntimeException if the leadership is not owned by this participant, or if the + * participant did not join participant pool via this client. // TODO: define exp type + */ + public void relinquishLeader(String leaderPath) { + } + + /** + * Get current leader. + * + * @param leaderPath The path for leader election. + * @return Returns the current leader. Return null if no Leader at a given point. + * @throws RuntimeException when leader path does not exist. // TODO: define exp type + */ + public String getLeader(String leaderPath) { + return null; + } + + /** + * Return a list of hosts in participant pool + * + * @param leaderPath The path for leader election. + * @return a list of participant(s) that tried to elect themselves as leader. The current leader + * is not included in the list. + * Return an empty list if + * 1. There is a leader for this path but there is no other participants + * 2. There is no leader for this path at the time of query + * @throws RuntimeException when leader path does not exist. // TODO: define exp type + */ + public List getParticipants(String leaderPath) { + return null; + } + + /** + * APIs to register/unregister listener to leader path. All the participants can listen to any + * leaderPath, Including leader going down or a new leader comes up. + * Whenever current leader for that leaderPath goes down (considering it's ephemeral entity which + * get's auto-deleted after TTL or session timeout) or a new leader comes up, it notifies all + * participants who have been listening on entryChange event. + * + * An listener will still be installed if the path does not exists yet. + * + * @param leaderPath The path for leader election that listener is interested for change. + * @param listener An implementation of LeaderElectionListenerInterface + * @return an boolean value indicating if registration is success. + */ + public boolean subscribeLeadershipChanges(String leaderPath, + LeaderElectionListenerInterface listener) { + return false; + } + + /** + * @param leaderPath The path for leader election that listener is no longer interested for change. + * @param listener An implementation of LeaderElectionListenerInterface + */ + public void unsubscribeLeadershipChanges(String leaderPath, + LeaderElectionListenerInterface listener) { + } +} + diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/recipes/leaderelection/LeaderElectionListenerInterface.java b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/leaderelection/LeaderElectionListenerInterface.java new file mode 100644 index 0000000000..0436e1eb09 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/leaderelection/LeaderElectionListenerInterface.java @@ -0,0 +1,32 @@ +package org.apache.helix.metaclient.recipes.leaderelection; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * It provides APIs for listener listening on events like a new leader is created or current + * leader node is deleted. + */ +public interface LeaderElectionListenerInterface { + // When new leader is elected: + // noLeader (null) -> has leader (new leader name) + // When existing leader not leader anymore: + // has Leader (prevleader name) -> no leader (null) + public void onLeadershipChange(String leaderPath, String prevLeader, String curLeader); +} \ No newline at end of file diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/recipes/leaderelection/LeaderInfo.java b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/leaderelection/LeaderInfo.java new file mode 100644 index 0000000000..9e817c1c97 --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/leaderelection/LeaderInfo.java @@ -0,0 +1,45 @@ +package org.apache.helix.metaclient.recipes.leaderelection; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.datamodel.DataRecord; + + +/** + * This is the data represent leader election info of a leader election path. + */ +public class LeaderInfo { + + private String _leaderElectionGroupName; + private final DataRecord _record; + + + public LeaderInfo( String leaderElectionGroupName) { + _leaderElectionGroupName = leaderElectionGroupName; + _record = new DataRecord(_leaderElectionGroupName); + } + + public enum LeaderAttribute { + LEADER_NAME, + PARTICIPANTS + } + + +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/recipes/lock/LockClient.java b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/lock/LockClient.java new file mode 100644 index 0000000000..cd5c3c297e --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/lock/LockClient.java @@ -0,0 +1,123 @@ +package org.apache.helix.metaclient.recipes.lock; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.api.Op; +import org.apache.helix.metaclient.datamodel.DataRecord; +import org.apache.helix.metaclient.exception.MetaClientException; +import org.apache.helix.metaclient.factories.MetaClientConfig; +import org.apache.helix.metaclient.impl.zk.factory.ZkMetaClientConfig; +import org.apache.helix.metaclient.impl.zk.factory.ZkMetaClientFactory; +import org.apache.helix.zookeeper.datamodel.serializer.ZNRecordSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; + +public class LockClient implements LockClientInterface, AutoCloseable { + private final MetaClientInterface _metaClient; + //NEW_METACLIENT is used to indicate whether the metaClient is created by the LockClient or not. + private static Boolean NEW_METACLIENT = false; + private static final Logger LOG = LoggerFactory.getLogger(LockClient.class); + + public LockClient(MetaClientConfig config) { + if (config == null) { + throw new IllegalArgumentException("MetaClientConfig cannot be null."); + } + LOG.info("Creating MetaClient for LockClient"); + if (MetaClientConfig.StoreType.ZOOKEEPER.equals(config.getStoreType())) { + ZkMetaClientConfig zkMetaClientConfig = new ZkMetaClientConfig.ZkMetaClientConfigBuilder(). + setConnectionAddress(config.getConnectionAddress()) + // Currently only support ZNRecordSerializer. TODO: make this configurable + .setZkSerializer((new ZNRecordSerializer())) + .build(); + _metaClient = new ZkMetaClientFactory().getMetaClient(zkMetaClientConfig); + _metaClient.connect(); + NEW_METACLIENT = true; + } else { + throw new MetaClientException("Unsupported store type: " + config.getStoreType()); + } + } + + public LockClient(MetaClientInterface client) { + if (client == null) { + throw new IllegalArgumentException("MetaClient cannot be null."); + } + _metaClient = client; + LOG.info("Connecting to existing MetaClient for LockClient"); + _metaClient.connect(); + } + + @Override + public void acquireLock(String key, LockInfo lockInfo, MetaClientInterface.EntryMode mode) { + _metaClient.create(key, lockInfo, mode); + } + + @Override + public void acquireLockWithTTL(String key, LockInfo lockInfo, long ttl) { + _metaClient.createWithTTL(key, lockInfo, ttl); + } + + @Override + public void renewTTLLock(String key) { + _metaClient.renewTTLNode(key); + } + + @Override + public void releaseLock(String key) { + MetaClientInterface.Stat stat = _metaClient.exists(key); + if (stat != null) { + int version = stat.getVersion(); + List ops = Arrays.asList( + Op.check(key, version), + Op.delete(key, version)); + _metaClient.transactionOP(ops); + if (_metaClient.exists(key) != null) { + throw new MetaClientException("Failed to release lock for key: " + key); + } + } + } + + @Override + public LockInfo retrieveLock(String key) { + MetaClientInterface.Stat stat = _metaClient.exists(key); + if (stat == null) { + return null; + } + //Create a new DataRecord from underlying record + DataRecord dataRecord = new DataRecord(_metaClient.get(key)); + //Create a new LockInfo from DataRecord + LockInfo lockInfo = new LockInfo(dataRecord, stat); + return lockInfo; + } + + @Override + public void close() { + if (NEW_METACLIENT) { + LOG.info("Closing created MetaClient for LockClient"); + } else { + LOG.warn("Closing existing MetaClient"); + } + _metaClient.disconnect(); + } +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/recipes/lock/LockClientInterface.java b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/lock/LockClientInterface.java new file mode 100644 index 0000000000..02de69569e --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/lock/LockClientInterface.java @@ -0,0 +1,63 @@ +package org.apache.helix.metaclient.recipes.lock; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.api.MetaClientInterface; + +public interface LockClientInterface { + /** + * Acquires a lock at key. + * @param key key to identify the entry + * @param info Metadata of the lock + * @param mode EntryMode identifying if the entry will be deleted upon client disconnect + * (Persistent, Ephemeral, or Container) + */ + void acquireLock(String key, LockInfo info, MetaClientInterface.EntryMode mode); + + /** + * Acquires a lock at key with a TTL. The lock will be deleted after the TTL. + * @param key key to identify the entry + * @param info Metadata of the lock + * @param ttl Time to live in milliseconds + */ + void acquireLockWithTTL(String key, LockInfo info, long ttl); + + /** + * Renews lock for a TTL Node. + * Will fail if key is an invalid path or isn't of type TTL. + * @param key key to identify the entry + */ + void renewTTLLock(String key); + + /** + * Releases the lock. + * Will fail if key is an invalid path. + * @param key key to identify the entry + */ + void releaseLock(String key); + + /** + * Obtains the metadata of a lock (the LockInfo). + * @param key key to identify the entry + * @return LockInfo object of the node at key. If fails to retrieve, return null. + * If other error, will raise exception. + */ + LockInfo retrieveLock(String key); +} diff --git a/meta-client/src/main/java/org/apache/helix/metaclient/recipes/lock/LockInfo.java b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/lock/LockInfo.java new file mode 100644 index 0000000000..26736f1b0c --- /dev/null +++ b/meta-client/src/main/java/org/apache/helix/metaclient/recipes/lock/LockInfo.java @@ -0,0 +1,256 @@ +package org.apache.helix.metaclient.recipes.lock; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.datamodel.DataRecord; + + +/** + * This structure represents a Lock node information, implemented using DataRecord + */ +@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +public class LockInfo extends DataRecord { + + // Default values for each attribute if there are no current values set by user + public static final String DEFAULT_LOCK_ID_TEXT = ""; + public static final String DEFAULT_OWNER_ID_TEXT = ""; + public static final String DEFAULT_CLIENT_ID_TEXT = ""; + public static final String DEFAULT_CLIENT_DATA = ""; + public static final long DEFAULT_GRANTED_AT_LONG = -1L; + public static final long DEFAULT_LAST_RENEWED_AT_LONG = -1L; + public static final long DEFAULT_TIMEOUT_DURATION = -1L; + private static final String DEFAULT_LOCK_INFO = "lockInfo."; + private DataRecord _dataRecord; + + /** + * The keys to lock information + */ + public enum LockInfoAttribute { + LOCK_ID, + OWNER_ID, + CLIENT_ID, + CLIENT_DATA, + GRANTED_AT, + LAST_RENEWED_AT, + TIMEOUT + } + + /** + * Initialize a default LockInfo instance + */ + public LockInfo() { + super(DEFAULT_LOCK_INFO); + _dataRecord = new DataRecord(DEFAULT_LOCK_INFO); + setLockInfoFields(DEFAULT_LOCK_ID_TEXT, DEFAULT_OWNER_ID_TEXT, DEFAULT_CLIENT_ID_TEXT, DEFAULT_CLIENT_DATA, DEFAULT_GRANTED_AT_LONG, + DEFAULT_LAST_RENEWED_AT_LONG, DEFAULT_TIMEOUT_DURATION); + } + + /** + * Initialize a LockInfo with a DataRecord, set all info fields to default data + * @param dataRecord The dataRecord contains lock node data that used to initialize the LockInfo + */ + public LockInfo(DataRecord dataRecord) { + this(); + if (dataRecord != null) { + String lockId = dataRecord.getSimpleField(LockInfoAttribute.LOCK_ID.name()); + String ownerId = dataRecord.getSimpleField(LockInfoAttribute.OWNER_ID.name()); + String clientId = dataRecord.getSimpleField(LockInfoAttribute.CLIENT_ID.name()); + String clientData = dataRecord.getSimpleField(LockInfoAttribute.CLIENT_DATA.name()); + long grantTime = dataRecord.getLongField(LockInfoAttribute.GRANTED_AT.name(), DEFAULT_GRANTED_AT_LONG); + long lastRenewalTime = + dataRecord.getLongField(LockInfoAttribute.LAST_RENEWED_AT.name(), DEFAULT_LAST_RENEWED_AT_LONG); + long timeout = dataRecord.getLongField(LockInfoAttribute.TIMEOUT.name(), DEFAULT_TIMEOUT_DURATION); + setLockInfoFields(lockId,ownerId, clientId, clientData, grantTime, + lastRenewalTime, timeout); + } + } + + /** + * Initialize a LockInfo with a DataRecord and a Stat, set all info fields to default data + * @param dataRecord The dataRecord contains lock node data that used to initialize the LockInfo + * @param stat The stat of the lock node + */ + public LockInfo(DataRecord dataRecord, MetaClientInterface.Stat stat) { + this(dataRecord); + //Synchronize the lockInfo with the stat + setGrantedAt(stat.getCreationTime()); + setLastRenewedAt(stat.getModifiedTime()); + } + + /** + * Initialize a LockInfo with data for each field, set all null info fields to default data + * @param lockId value of LOCK_ID attribute + * @param ownerId value of OWNER_ID attribute + * @param clientId value of CLIENT_ID attribute + * @param clientData value of CLIENT_DATA attribute + * @param grantTime the time the lock was granted + * @param lastRenewalTime the last time the lock was renewed + * @param timeout value of TIMEOUT attribute + */ + public LockInfo(String lockId, String ownerId, String clientId, + String clientData, long grantTime, long lastRenewalTime, long timeout) { + this(); + setLockInfoFields(lockId, ownerId, clientId, clientData, grantTime, lastRenewalTime, timeout); + } + + /** + * Set each field of lock info to user provided values if the values + * are not null, null values are set to default values + * @param lockId value of LOCK_ID attribute + * @param ownerId value of OWNER_ID attribute + * @param clientId value of CLIENT_ID attribute + * @param clientData value of CLIENT_DATA attribute + * @param grantTime the time the lock was granted + * @param lastRenewalTime the last time the lock was renewed + * @param timeout value of TIMEOUT attribute + */ + private void setLockInfoFields(String lockId, String ownerId, String clientId, String clientData, long grantTime, long lastRenewalTime, + long timeout) { + setLockId(lockId); + setOwnerId(ownerId); + setClientId(clientId); + setClientData(clientData); + setGrantedAt(grantTime); + setLastRenewedAt(lastRenewalTime); + setTimeout(timeout); + } + + /** + * Set the value for LOCK_ID attribute of the lock + * @param lockId Is a unique identifier representing the lock. + * It is created by the lockClient and a new one is created for each time the lock is acquired. + */ + public void setLockId(String lockId) { + _dataRecord.setSimpleField(LockInfoAttribute.LOCK_ID.name(), lockId == null ? DEFAULT_LOCK_ID_TEXT : lockId); + } + + /** + * Get the value for OWNER_ID attribute of the lock + * @param ownerId Represents the initiator of the lock, created by the client. + * A service can have multiple ownerId's as long as acquire and release are called + * by the same owner. + */ + public void setOwnerId(String ownerId) { + _dataRecord.setSimpleField(LockInfoAttribute.OWNER_ID.name(), ownerId == null ? DEFAULT_OWNER_ID_TEXT : ownerId); + } + + /** + * Get the value for CLIENT_ID attribute of the lock + * @param clientId Unique identifier that represents who will get the lock (the client). + */ + public void setClientId(String clientId) { + _dataRecord.setSimpleField(LockInfoAttribute.CLIENT_ID.name(), clientId == null ? DEFAULT_CLIENT_ID_TEXT : clientId); + } + + /** + * Get the value for CLIENT_DATA attribute of the lock + * @param clientData String representing the serialized data object + */ + public void setClientData(String clientData) { + _dataRecord.setSimpleField(LockInfoAttribute.CLIENT_DATA.name(), clientData == null ? DEFAULT_CLIENT_DATA : clientData); + } + + /** + * Get the value for GRANTED_AT attribute of the lock + * @param grantTime Long representing the time at which the lock was granted + */ + public void setGrantedAt(Long grantTime) { + _dataRecord.setLongField(LockInfoAttribute.GRANTED_AT.name(), grantTime); + } + + /** + * Get the value for LAST_RENEWED_AT attribute of the lock + * @param lastRenewalTime Long representing the time at which the lock was last renewed + */ + public void setLastRenewedAt(Long lastRenewalTime) { + _dataRecord.setLongField(LockInfoAttribute.LAST_RENEWED_AT.name(), lastRenewalTime); + } + + /** + * Get the value for TIMEOUT attribute of the lock + * @param timeout Long representing the duration of a lock in milliseconds. + */ + public void setTimeout(long timeout) { + // Always store the timeout value in milliseconds for the sake of simplicity + _dataRecord.setLongField(LockInfoAttribute.TIMEOUT.name(), timeout); + } + + /** + * Get the value for OWNER_ID attribute of the lock + * @return the owner id of the lock, {@link #DEFAULT_OWNER_ID_TEXT} if there is no owner id set + */ + public String getOwnerId() { + return _dataRecord.getStringField(LockInfoAttribute.OWNER_ID.name(), DEFAULT_OWNER_ID_TEXT); + } + + /** + * Get the value for CLIENT_ID attribute of the lock + * @return the client id of the lock, {@link #DEFAULT_CLIENT_ID_TEXT} if there is no client id set + */ + public String getClientId() { + return _dataRecord.getStringField(LockInfoAttribute.CLIENT_ID.name(), DEFAULT_CLIENT_ID_TEXT); + } + + /** + * Get the value for LOCK_ID attribute of the lock + * @return the id of the lock, {@link #DEFAULT_LOCK_ID_TEXT} if there is no lock id set + */ + public String getLockId() { + return _dataRecord.getStringField(LockInfoAttribute.LOCK_ID.name(), DEFAULT_LOCK_ID_TEXT); + } + + /** + * Get value of CLIENT_DATA + * @return the string representing the serialized client data, {@link #DEFAULT_CLIENT_DATA} + * if there is no client data set. + */ + public String getClientData() { + return _dataRecord.getStringField(LockInfoAttribute.CLIENT_DATA.name(), DEFAULT_CLIENT_DATA); + } + + /** + * Get the time the lock was granted on + * @return the grant time of the lock, {@link #DEFAULT_GRANTED_AT_LONG} + * if there is no grant time set + */ + public Long getGrantedAt() { + return _dataRecord.getLongField(LockInfoAttribute.GRANTED_AT.name(), DEFAULT_GRANTED_AT_LONG); + } + + /** + * Get the last time the lock was renewed + * @return the last renewal time of the lock, {@link #DEFAULT_LAST_RENEWED_AT_LONG} + * if there is no renewal time set + */ + public Long getLastRenewedAt() { + return _dataRecord.getLongField(LockInfoAttribute.LAST_RENEWED_AT.name(), DEFAULT_LAST_RENEWED_AT_LONG); + } + + /** + * Get the value for TIMEOUT attribute of the lock + * @return the expiring time of the lock, {@link #DEFAULT_TIMEOUT_DURATION} if there is no timeout set + */ + public long getTimeout() { + return _dataRecord.getLongField(LockInfoAttribute.TIMEOUT.name(), DEFAULT_TIMEOUT_DURATION); + } + +} diff --git a/meta-client/src/test/conf/testng.xml b/meta-client/src/test/conf/testng.xml new file mode 100644 index 0000000000..b5e3518d18 --- /dev/null +++ b/meta-client/src/test/conf/testng.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestConnectStateChangeListenerAndRetry.java b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestConnectStateChangeListenerAndRetry.java new file mode 100644 index 0000000000..36b9b21319 --- /dev/null +++ b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestConnectStateChangeListenerAndRetry.java @@ -0,0 +1,180 @@ +package org.apache.helix.metaclient.impl.zk; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.Date; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.helix.metaclient.api.ConnectStateChangeListener; +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.impl.zk.factory.ZkMetaClientConfig; +import org.apache.helix.metaclient.policy.ExponentialBackoffReconnectPolicy; +import org.apache.helix.zookeeper.zkclient.ZkClient; +import org.apache.helix.zookeeper.zkclient.ZkServer; +import org.apache.zookeeper.WatchedEvent; +import org.apache.zookeeper.Watcher; +import org.testng.Assert; +import org.testng.annotations.AfterSuite; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Test; + +import static org.apache.helix.metaclient.constants.MetaClientConstants.DEFAULT_INIT_EXP_BACKOFF_RETRY_INTERVAL_MS; +import static org.apache.helix.metaclient.constants.MetaClientConstants.DEFAULT_MAX_EXP_BACKOFF_RETRY_INTERVAL_MS; + + +public class TestConnectStateChangeListenerAndRetry { + protected static final String ZK_ADDR = "localhost:2181"; + protected static ZkServer _zkServer; + + private static final long AUTO_RECONNECT_TIMEOUT_MS_FOR_TEST = 3 * 1000; + private static final long AUTO_RECONNECT_WAIT_TIME_WITHIN = 1 * 1000; + private static final long AUTO_RECONNECT_WAIT_TIME_EXD = 5 * 1000; + + /** + * Simulate a zk state change by calling {@link ZkClient#process(WatchedEvent)} directly + * This need to be done in a separate thread to simulate ZkClient eventThread. + */ + private static void simulateZkStateReconnected(ZkClient zkClient) throws InterruptedException { + WatchedEvent event = + new WatchedEvent(Watcher.Event.EventType.None, Watcher.Event.KeeperState.Disconnected, + null); + zkClient.process(event); + + Thread.sleep(AUTO_RECONNECT_WAIT_TIME_WITHIN); + + event = new WatchedEvent(Watcher.Event.EventType.None, Watcher.Event.KeeperState.SyncConnected, + null); + zkClient.process(event); + } + + @BeforeSuite + public void prepare() { + System.out.println("START TestConnectStateChangeListenerAndRetry at " + new Date(System.currentTimeMillis())); + // start local zookeeper server + _zkServer = ZkMetaClientTestBase.startZkServer(ZK_ADDR); + } + + @AfterSuite + public void cleanUp() { + System.out.println("END TestConnectStateChangeListenerAndRetry at " + new Date(System.currentTimeMillis())); + } + + @Test + public void testConnectState() { + System.out.println("STARTING TestConnectStateChangeListenerAndRetry.testConnectState at " + new Date(System.currentTimeMillis())); + try (ZkMetaClient zkMetaClient = createZkMetaClientReconnectTest()) { + zkMetaClient.connect(); + zkMetaClient.connect(); + Assert.fail("The second connect should throw IllegalStateException"); + } catch (Exception ex) { + Assert.assertTrue(ex instanceof IllegalStateException); + Assert.assertEquals(ex.getMessage(), "ZkClient is not in init state. connect() has already been called."); + } + System.out.println("END TestConnectStateChangeListenerAndRetry.testConnectState at " + new Date(System.currentTimeMillis())); + } + + // test mock zkclient event + @Test(dependsOnMethods = "testConnectState") + public void testReConnectSucceed() throws InterruptedException { + System.out.println("STARTING TestConnectStateChangeListenerAndRetry.testReConnectSucceed at " + new Date(System.currentTimeMillis())); + try (ZkMetaClient zkMetaClient = createZkMetaClientReconnectTest()) { + CountDownLatch countDownLatch = new CountDownLatch(1); + + zkMetaClient.connect(); + // We need a separate thread to simulate reconnect. In ZkClient there is assertion to check + // reconnect and and CRUDs are not in the same thread. (So one does not block another) + Executors.newSingleThreadExecutor().execute(new Runnable() { + @Override + public void run() { + try { + simulateZkStateReconnected(zkMetaClient.getZkClient()); + } catch (InterruptedException e) { + Assert.fail("Exception in simulateZkStateReconnected", e); + } + countDownLatch.countDown(); + } + }); + countDownLatch.await(5000, TimeUnit.SECONDS); + Thread.sleep(AUTO_RECONNECT_WAIT_TIME_EXD); + // When ZK reconnect happens within timeout window, zkMetaClient should ba able to perform CRUD. + Assert.assertTrue(zkMetaClient.getZkClient().getConnection().getZookeeperState().isConnected()); + zkMetaClient.create("/key", "value"); + Assert.assertEquals(zkMetaClient.get("/key"), "value"); + } + System.out.println("END TestConnectStateChangeListenerAndRetry.testReConnectSucceed at " + new Date(System.currentTimeMillis())); + } + + @Test(dependsOnMethods = "testReConnectSucceed") + public void testConnectStateChangeListener() throws Exception { + System.out.println("START TestConnectStateChangeListenerAndRetry.testConnectStateChangeListener at " + new Date(System.currentTimeMillis())); + try (ZkMetaClient zkMetaClient = createZkMetaClientReconnectTest()) { + CountDownLatch countDownLatch = new CountDownLatch(1); + final MetaClientInterface.ConnectState[] connectState = + new MetaClientInterface.ConnectState[2]; + ConnectStateChangeListener listener = new ConnectStateChangeListener() { + @Override + public void handleConnectStateChanged(MetaClientInterface.ConnectState prevState, + MetaClientInterface.ConnectState currentState) throws Exception { + connectState[0] = prevState; + connectState[1] = currentState; + countDownLatch.countDown(); + } + + @Override + public void handleConnectionEstablishmentError(Throwable error) throws Exception { + + } + }; + Assert.assertTrue(zkMetaClient.subscribeStateChanges(listener)); + zkMetaClient.connect(); + countDownLatch.await(5000, TimeUnit.SECONDS); + Assert.assertEquals(connectState[0], MetaClientInterface.ConnectState.NOT_CONNECTED); + Assert.assertEquals(connectState[1], MetaClientInterface.ConnectState.CONNECTED); + + _zkServer.shutdown(); + Thread.sleep(AUTO_RECONNECT_WAIT_TIME_EXD); + Assert.assertEquals(connectState[0], MetaClientInterface.ConnectState.CONNECTED); + Assert.assertEquals(connectState[1], MetaClientInterface.ConnectState.DISCONNECTED); + + try { + zkMetaClient.create("/key", "value"); + Assert.fail("Create call after close should throw IllegalStateException"); + } catch (Exception ex) { + Assert.assertTrue(ex.getCause() instanceof IllegalStateException); + } + } + System.out.println("END TestConnectStateChangeListenerAndRetry.testConnectStateChangeListener at " + new Date(System.currentTimeMillis())); + } + + static ZkMetaClient createZkMetaClientReconnectTest() { + ZkMetaClientConfig config = + new ZkMetaClientConfig.ZkMetaClientConfigBuilder().setConnectionAddress(ZK_ADDR) + .setMetaClientReconnectPolicy( + new ExponentialBackoffReconnectPolicy( + AUTO_RECONNECT_TIMEOUT_MS_FOR_TEST)) + .build(); + return new ZkMetaClient<>(config); + } +} diff --git a/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestUtil.java b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestUtil.java new file mode 100644 index 0000000000..1296a72f30 --- /dev/null +++ b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestUtil.java @@ -0,0 +1,96 @@ +package org.apache.helix.metaclient.impl.zk; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.helix.zookeeper.api.client.RealmAwareZkClient; +import org.apache.helix.zookeeper.zkclient.ZkClient; +import org.apache.helix.zookeeper.zkclient.ZkConnection; +import org.apache.zookeeper.Watcher; +import org.apache.zookeeper.ZooKeeper; + + +public class TestUtil { + + static java.lang.reflect.Field getField(Class clazz, String fieldName) + throws NoSuchFieldException { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + Class superClass = clazz.getSuperclass(); + if (superClass == null) { + throw e; + } + return getField(superClass, fieldName); + } + } + + public static Map> getZkWatch(RealmAwareZkClient client) + throws Exception { + Map> lists = new HashMap>(); + ZkClient zkClient = (ZkClient) client; + + ZkConnection connection = ((ZkConnection) zkClient.getConnection()); + ZooKeeper zk = connection.getZookeeper(); + + java.lang.reflect.Field field = getField(zk.getClass(), "watchManager"); + field.setAccessible(true); + Object watchManager = field.get(zk); + + java.lang.reflect.Field field2 = getField(watchManager.getClass(), "dataWatches"); + field2.setAccessible(true); + HashMap> dataWatches = + (HashMap>) field2.get(watchManager); + + field2 = getField(watchManager.getClass(), "existWatches"); + field2.setAccessible(true); + HashMap> existWatches = + (HashMap>) field2.get(watchManager); + + field2 = getField(watchManager.getClass(), "childWatches"); + field2.setAccessible(true); + HashMap> childWatches = + (HashMap>) field2.get(watchManager); + + field2 = getField(watchManager.getClass(), "persistentWatches"); + field2.setAccessible(true); + HashMap> persistentWatches = + (HashMap>) field2.get(watchManager); + + field2 = getField(watchManager.getClass(), "persistentRecursiveWatches"); + field2.setAccessible(true); + HashMap> persistentRecursiveWatches = + (HashMap>) field2.get(watchManager); + + + lists.put("dataWatches", new ArrayList<>(dataWatches.keySet())); + lists.put("existWatches", new ArrayList<>(existWatches.keySet())); + lists.put("childWatches", new ArrayList<>(childWatches.keySet())); + lists.put("persistentWatches", new ArrayList<>(persistentWatches.keySet())); + lists.put("persistentRecursiveWatches", new ArrayList<>(persistentRecursiveWatches.keySet())); + + return lists; + } +} \ No newline at end of file diff --git a/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestZkMetaClient.java b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestZkMetaClient.java new file mode 100644 index 0000000000..69724c9fe0 --- /dev/null +++ b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestZkMetaClient.java @@ -0,0 +1,570 @@ +package org.apache.helix.metaclient.impl.zk; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.api.ChildChangeListener; +import org.apache.helix.metaclient.api.DataUpdater; +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.exception.MetaClientException; +import org.apache.helix.metaclient.api.DirectChildChangeListener; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Map; +import java.util.HashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang3.NotImplementedException; +import org.apache.helix.metaclient.api.ConnectStateChangeListener; +import org.apache.helix.metaclient.api.DataChangeListener; +import org.apache.helix.metaclient.api.Op; +import org.apache.helix.metaclient.api.OpResult; +import org.apache.helix.metaclient.exception.MetaClientNoNodeException; +import org.apache.helix.metaclient.impl.zk.factory.ZkMetaClientConfig; +import org.apache.zookeeper.KeeperException; +import org.testng.Assert; +import org.testng.annotations.Test; + +import static org.apache.helix.metaclient.api.DataChangeListener.ChangeType.ENTRY_UPDATE; +import static org.apache.helix.metaclient.api.MetaClientInterface.EntryMode.CONTAINER; +import static org.apache.helix.metaclient.api.MetaClientInterface.EntryMode.PERSISTENT; + + +public class TestZkMetaClient extends ZkMetaClientTestBase{ + + private static final String TRANSACTION_TEST_PARENT_PATH = "/transactionOpTestPath"; + private static final String TEST_INVALID_PATH = "/_invalid/a/b/c"; + private static final int DEFAULT_LISTENER_WAIT_TIMEOUT = 5000; + + private final Object _syncObject = new Object(); + + @Test + public void testCreate() { + final String key = "/TestZkMetaClient_testCreate"; + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + zkMetaClient.create(key, ENTRY_STRING_VALUE); + Assert.assertNotNull(zkMetaClient.exists(key)); + + try { + zkMetaClient.create("a/b/c", "invalid_path"); + Assert.fail("Should have failed with incorrect path."); + } catch (Exception ignored) { + } + } + } + + @Test + public void testCreateContainer() { + final String key = "/TestZkMetaClient_testCreateContainer"; + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + zkMetaClient.create(key, ENTRY_STRING_VALUE, CONTAINER); + Assert.assertNotNull(zkMetaClient.exists(key)); + } + } + + @Test + public void testCreateTTL() { + final String key = "/TestZkMetaClient_testTTL"; + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + zkMetaClient.createWithTTL(key, ENTRY_STRING_VALUE, 1000); + Assert.assertNotNull(zkMetaClient.exists(key)); + } + } + + @Test + public void testRenewTTL() { + final String key = "/TestZkMetaClient_testRenewTTL_1"; + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + zkMetaClient.createWithTTL(key, ENTRY_STRING_VALUE, 10000); + Assert.assertNotNull(zkMetaClient.exists(key)); + MetaClientInterface.Stat stat = zkMetaClient.exists(key); + zkMetaClient.renewTTLNode(key); + // Renewing a ttl node changes the nodes modified_time. Should be different + // from the time the node was created. + Assert.assertNotSame(stat.getCreationTime(), stat.getModifiedTime()); + try { + zkMetaClient.renewTTLNode(TEST_INVALID_PATH); + } catch (MetaClientNoNodeException ignored) { + } + } + } + + @Test + public void testGet() { + final String key = "/TestZkMetaClient_testGet"; + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + String value; + zkMetaClient.create(key, ENTRY_STRING_VALUE); + String dataValue = zkMetaClient.get(key); + Assert.assertEquals(dataValue, ENTRY_STRING_VALUE); + + value = zkMetaClient.get(key + "/a/b/c"); + Assert.assertNull(value); + + zkMetaClient.delete(key); + + value = zkMetaClient.get(key); + Assert.assertNull(value); + } + } + + @Test + public void testSet() { + final String key = "/TestZkMetaClient_testSet"; + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + zkMetaClient.create(key, ENTRY_STRING_VALUE); + String testValueV1 = ENTRY_STRING_VALUE + "-v1"; + String testValueV2 = ENTRY_STRING_VALUE + "-v2"; + + // test set() with no expected version and validate result. + zkMetaClient.set(key, testValueV1, -1); + Assert.assertEquals(zkMetaClient.get(key), testValueV1); + MetaClientInterface.Stat entryStat = zkMetaClient.exists(key); + Assert.assertEquals(entryStat.getVersion(), 1); + Assert.assertEquals(entryStat.getEntryType().name(), PERSISTENT.name()); + + // test set() with expected version and validate result and new version number + zkMetaClient.set(key, testValueV2, 1); + entryStat = zkMetaClient.exists(key); + Assert.assertEquals(zkMetaClient.get(key), testValueV2); + Assert.assertEquals(entryStat.getVersion(), 2); + + // test set() with a wrong version + try { + zkMetaClient.set(key, "test-node-changed", 10); + Assert.fail("No reach."); + } catch (MetaClientException ex) { + Assert.assertEquals(ex.getClass().getName(), + "org.apache.helix.metaclient.exception.MetaClientBadVersionException"); + } + zkMetaClient.delete(key); + } + } + + @Test + public void testUpdate() { + final String key = "/TestZkMetaClient_testUpdate"; + ZkMetaClientConfig config = + new ZkMetaClientConfig.ZkMetaClientConfigBuilder().setConnectionAddress(ZK_ADDR).build(); + try (ZkMetaClient zkMetaClient = new ZkMetaClient<>(config)) { + zkMetaClient.connect(); + int initValue = 3; + zkMetaClient.create(key, initValue); + MetaClientInterface.Stat entryStat = zkMetaClient.exists(key); + Assert.assertEquals(entryStat.getVersion(), 0); + + // test update() and validate entry value and version + Integer newData = zkMetaClient.update(key, new DataUpdater() { + @Override + public Integer update(Integer currentData) { + return currentData + 1; + } + }); + Assert.assertEquals((int) newData, (int) initValue + 1); + + entryStat = zkMetaClient.exists(key); + Assert.assertEquals(entryStat.getVersion(), 1); + + newData = zkMetaClient.update(key, new DataUpdater() { + + @Override + public Integer update(Integer currentData) { + return currentData + 1; + } + }); + + entryStat = zkMetaClient.exists(key); + Assert.assertEquals(entryStat.getVersion(), 2); + Assert.assertEquals((int) newData, (int) initValue + 2); + zkMetaClient.delete(key); + } + } + + @Test + public void testGetAndCountChildrenAndRecursiveDelete() { + final String key = "/TestZkMetaClient_testGetAndCountChildren"; + List childrenNames = Arrays.asList("/c1", "/c2", "/c3"); + + // create child nodes and validate retrieved children count and names + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + zkMetaClient.create(key, ENTRY_STRING_VALUE); + Assert.assertEquals(zkMetaClient.countDirectChildren(key), 0); + for (String str : childrenNames) { + zkMetaClient.create(key + str, ENTRY_STRING_VALUE); + } + + List retrievedChildrenNames = zkMetaClient.getDirectChildrenKeys(key); + Assert.assertEquals(retrievedChildrenNames.size(), childrenNames.size()); + Set childrenNameSet = new HashSet<>(childrenNames); + for (String str : retrievedChildrenNames) { + Assert.assertTrue(childrenNameSet.contains("/" + str)); + } + + // recursive delete and validate + Assert.assertEquals(zkMetaClient.countDirectChildren(key), childrenNames.size()); + Assert.assertNotNull(zkMetaClient.exists(key)); + zkMetaClient.recursiveDelete(key); + Assert.assertNull(zkMetaClient.exists(key)); + } + } + + @Test + public void testDataChangeListenerTriggerWithZkWatcher() throws Exception { + final String path = "/TestZkMetaClient_testTriggerWithZkWatcher"; + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + MockDataChangeListener listener = new MockDataChangeListener(); + zkMetaClient.subscribeDataChange(path, listener, false); + zkMetaClient.create(path, "test-node"); + int expectedCallCount = 0; + synchronized (_syncObject) { + while (listener.getTriggeredCount() == expectedCallCount) { + _syncObject.wait(DEFAULT_TIMEOUT_MS); + } + expectedCallCount++; + Assert.assertEquals(listener.getTriggeredCount(), expectedCallCount); + Assert.assertEquals(listener.getLastEventType(), DataChangeListener.ChangeType.ENTRY_CREATED); + } + zkMetaClient.set(path, "test-node-changed", -1); + synchronized (_syncObject) { + while (listener.getTriggeredCount() == expectedCallCount) { + _syncObject.wait(DEFAULT_TIMEOUT_MS); + } + expectedCallCount++; + Assert.assertEquals(listener.getTriggeredCount(), expectedCallCount); + Assert.assertEquals(listener.getLastEventType(), DataChangeListener.ChangeType.ENTRY_UPDATE); + } + zkMetaClient.delete(path); + synchronized (_syncObject) { + while (listener.getTriggeredCount() == expectedCallCount) { + _syncObject.wait(DEFAULT_TIMEOUT_MS); + } + expectedCallCount++; + Assert.assertEquals(listener.getTriggeredCount(), expectedCallCount); + Assert.assertEquals(listener.getLastEventType(), DataChangeListener.ChangeType.ENTRY_DELETED); + } + // unregister listener, expect no more call + zkMetaClient.unsubscribeDataChange(path, listener); + zkMetaClient.create(path, "test-node"); + synchronized (_syncObject) { + _syncObject.wait(DEFAULT_TIMEOUT_MS); + Assert.assertEquals(listener.getTriggeredCount(), expectedCallCount); + } + // register a new non-persistent listener + try { + zkMetaClient.subscribeOneTimeDataChange(path, new MockDataChangeListener(), false); + Assert.fail("One-time listener is not supported, NotImplementedException should be thrown."); + } catch (NotImplementedException e) { + // expected + } + } + } + + @Test(dependsOnMethods = "testDataChangeListenerTriggerWithZkWatcher") + public void testMultipleDataChangeListeners() throws Exception { + final String basePath = "/TestZkMetaClient_testMultipleDataChangeListeners"; + final int count = 5; + final String testData = "test-data"; + final AtomicBoolean dataExpected = new AtomicBoolean(true); + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + Map> listeners = new HashMap<>(); + CountDownLatch countDownLatch = new CountDownLatch(count); + zkMetaClient.create(basePath + "_1", testData); + // create paths + for (int i = 0; i < 2; i++) { + String path = basePath + "_" + i; + listeners.put(path, new HashSet<>()); + // 5 listeners for each path + for (int j = 0; j < count; j++) { + DataChangeListener listener = new DataChangeListener() { + @Override + public void handleDataChange(String key, Object data, ChangeType changeType) { + countDownLatch.countDown(); + dataExpected.set(dataExpected.get() && testData.equals(data)); + } + }; + listeners.get(path).add(listener); + zkMetaClient.subscribeDataChange(path, listener, false); + } + } + zkMetaClient.set(basePath + "_1", testData, -1); + Assert.assertTrue(countDownLatch.await(DEFAULT_LISTENER_WAIT_TIMEOUT, TimeUnit.MILLISECONDS)); + Assert.assertTrue(dataExpected.get()); + } + } + + @Test + public void testDirectChildChangeListener() throws Exception { + final String basePath = "/TestZkMetaClient_testDirectChildChangeListener"; + final int count = 1000; + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + CountDownLatch countDownLatch = new CountDownLatch(count); + DirectChildChangeListener listener = new DirectChildChangeListener() { + @Override + public void handleDirectChildChange(String key) throws Exception { + countDownLatch.countDown(); + } + }; + zkMetaClient.create(basePath, ""); + Assert.assertTrue( + zkMetaClient.subscribeDirectChildChange(basePath, listener, false) + .isRegistered()); + for(int i=0; i<1000; ++i){ + zkMetaClient.create(basePath + "/child_" +i, "test-data"); + } + // Verify no one time watcher is registered. Only one persist listener is registered. + Map> watchers = TestUtil.getZkWatch(zkMetaClient.getZkClient()); + Assert.assertEquals(watchers.get("persistentWatches").size(), 1); + Assert.assertEquals(watchers.get("persistentWatches").get(0), basePath); + Assert.assertEquals(watchers.get("childWatches").size(), 0); + Assert.assertEquals(watchers.get("dataWatches").size(), 0); + Assert.assertTrue(countDownLatch.await(DEFAULT_LISTENER_WAIT_TIMEOUT, TimeUnit.MILLISECONDS)); + + zkMetaClient.unsubscribeDirectChildChange(basePath, listener); + // verify that no listener is registered on any path + watchers = TestUtil.getZkWatch(zkMetaClient.getZkClient()); + Assert.assertEquals(watchers.get("persistentWatches").size(), 0); + Assert.assertEquals(watchers.get("childWatches").size(), 0); + Assert.assertEquals(watchers.get("dataWatches").size(), 0); + } + } + + @Test + public void testDataChangeListener() throws Exception { + final String basePath = "/TestZkMetaClient_testDataChangeListener"; + final int count = 200; + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + CountDownLatch countDownLatch = new CountDownLatch(count); + DataChangeListener listener = new DataChangeListener() { + + @Override + public void handleDataChange(String key, Object data, ChangeType changeType) + throws Exception { + if(changeType == ENTRY_UPDATE) { + countDownLatch.countDown(); + } + } + }; + zkMetaClient.create(basePath, ""); + Assert.assertTrue( + zkMetaClient.subscribeDataChange(basePath, listener, false) + ); + // Verify no one time watcher is registered. Only one persist listener is registered. + Map> watchers = TestUtil.getZkWatch(zkMetaClient.getZkClient()); + Assert.assertEquals(watchers.get("persistentWatches").size(), 1); + Assert.assertEquals(watchers.get("persistentWatches").get(0), basePath); + Assert.assertEquals(watchers.get("childWatches").size(), 0); + Assert.assertEquals(watchers.get("dataWatches").size(), 0); + + for (int i=0; i<200; ++i) { + zkMetaClient.set(basePath, "data7" + i, -1); + } + Assert.assertTrue(countDownLatch.await(DEFAULT_LISTENER_WAIT_TIMEOUT, TimeUnit.MILLISECONDS)); + + + zkMetaClient.unsubscribeDataChange(basePath, listener); + // verify that no listener is registered on any path + watchers = TestUtil.getZkWatch(zkMetaClient.getZkClient()); + Assert.assertEquals(watchers.get("persistentWatches").size(), 0); + Assert.assertEquals(watchers.get("childWatches").size(), 0); + Assert.assertEquals(watchers.get("dataWatches").size(), 0); + + } + } + + @Test + public void testChildChangeListener() throws Exception { + final String basePath = "/TestZkMetaClient_testChildChangeListener"; + final int count = 100; + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + CountDownLatch countDownLatch = new CountDownLatch(count*4); + ChildChangeListener listener = new ChildChangeListener() { + + @Override + public void handleChildChange(String changedPath, ChangeType changeType) throws Exception { + countDownLatch.countDown(); + + } + }; + zkMetaClient.create(basePath, ""); + Assert.assertTrue( + zkMetaClient.subscribeChildChanges(basePath, listener, false) + ); + + DataChangeListener dummyDataListener = new DataChangeListener() { + @Override + public void handleDataChange(String key, Object data, ChangeType changeType) + throws Exception { + } + }; + try { + zkMetaClient.subscribeDataChange(basePath, dummyDataListener, false); + Assert.fail("subscribeDataChange should throw exception"); + } catch (UnsupportedOperationException ex) { + // we are expecting a UnsupportedOperationException, continue with test. + } + + DirectChildChangeListener dummyCldListener = new DirectChildChangeListener() { + @Override + public void handleDirectChildChange(String key) throws Exception { + + } + }; + try { + zkMetaClient.subscribeDirectChildChange(basePath, dummyCldListener, false); + } catch ( Exception ex) { + Assert.assertEquals(ex.getClass().getName(), "java.lang.UnsupportedOperationException"); + } + + // Verify no one time watcher is registered. Only one persist listener is registered. + Map> watchers = TestUtil.getZkWatch(zkMetaClient.getZkClient()); + Assert.assertEquals(watchers.get("persistentRecursiveWatches").size(), 1); + Assert.assertEquals(watchers.get("persistentRecursiveWatches").get(0), basePath); + Assert.assertEquals(watchers.get("persistentWatches").size(), 0); + Assert.assertEquals(watchers.get("childWatches").size(), 0); + Assert.assertEquals(watchers.get("dataWatches").size(), 0); + + for (int i=0; i zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + + //Create Nodes + List ops = Arrays.asList( + Op.create(TRANSACTION_TEST_PARENT_PATH, new byte[0], MetaClientInterface.EntryMode.PERSISTENT), + Op.create(TRANSACTION_TEST_PARENT_PATH + test_name, new byte[0], MetaClientInterface.EntryMode.PERSISTENT), + Op.delete(TRANSACTION_TEST_PARENT_PATH + test_name, -1), + Op.create(TRANSACTION_TEST_PARENT_PATH + test_name, new byte[0], MetaClientInterface.EntryMode.PERSISTENT), + Op.set(TRANSACTION_TEST_PARENT_PATH + test_name, new byte[0], -1)); + + //Execute transactional support on operations + List opResults = zkMetaClient.transactionOP(ops); + + //Verify opResults types + Assert.assertTrue(opResults.get(0) instanceof OpResult.CreateResult); + Assert.assertTrue(opResults.get(1) instanceof OpResult.CreateResult); + Assert.assertTrue(opResults.get(2) instanceof OpResult.DeleteResult); + Assert.assertTrue(opResults.get(4) instanceof OpResult.SetDataResult); + + //Verify paths have been created + MetaClientInterface.Stat entryStat = zkMetaClient.exists(TRANSACTION_TEST_PARENT_PATH + test_name); + Assert.assertNotNull(entryStat, "Path should have been created."); + + //Cleanup + zkMetaClient.recursiveDelete(TRANSACTION_TEST_PARENT_PATH); + if (zkMetaClient.exists(TRANSACTION_TEST_PARENT_PATH) != null) { + Assert.fail("Parent Path should have been removed."); + } + } + } + + /** + * This test calls transactionOp on an invalid path. + * It checks that the invalid path has not been created to verify the + * "all or nothing" behavior of transactionOp. + * @throws KeeperException + */ + @Test(dependsOnMethods = "testTransactionOps") + public void testTransactionFail() { + String test_name = "/test_transaction_fail"; + try(ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + //Create Nodes + List ops = Arrays.asList( + Op.create(TRANSACTION_TEST_PARENT_PATH, new byte[0], MetaClientInterface.EntryMode.PERSISTENT), + Op.create(TRANSACTION_TEST_PARENT_PATH + test_name, new byte[0], MetaClientInterface.EntryMode.PERSISTENT), + Op.create(TEST_INVALID_PATH, new byte[0], MetaClientInterface.EntryMode.PERSISTENT)); + + try { + zkMetaClient.transactionOP(ops); + Assert.fail( + "Should have thrown an exception. Cannot run transactional create OP on incorrect path."); + } catch (Exception e) { + MetaClientInterface.Stat entryStat = zkMetaClient.exists(TRANSACTION_TEST_PARENT_PATH); + Assert.assertNull(entryStat); + } + } + } + + private class MockDataChangeListener implements DataChangeListener { + private final AtomicInteger _triggeredCount = new AtomicInteger(0); + private volatile ChangeType _lastEventType; + + @Override + public void handleDataChange(String key, Object data, ChangeType changeType) { + _triggeredCount.getAndIncrement(); + _lastEventType = changeType; + synchronized (_syncObject) { + _syncObject.notifyAll(); + } + } + + int getTriggeredCount() { + return _triggeredCount.get(); + } + + ChangeType getLastEventType() { + return _lastEventType; + } + } +} diff --git a/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestZkMetaClientAsyncOperations.java b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestZkMetaClientAsyncOperations.java new file mode 100644 index 0000000000..fbfd8ed355 --- /dev/null +++ b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestZkMetaClientAsyncOperations.java @@ -0,0 +1,233 @@ +package org.apache.helix.metaclient.impl.zk; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; + +import org.apache.helix.metaclient.api.AsyncCallback; +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.zookeeper.KeeperException; +import org.testng.Assert; +import org.testng.annotations.Test; + + +public class TestZkMetaClientAsyncOperations extends ZkMetaClientTestBase { + + static TestAsyncContext[] asyncContext = new TestAsyncContext[1]; + static final String entryKey = "/TestAsyncEntryKey"; + static final String nonExistsEntry = "/a/b/c"; + static final long LATCH_WAIT_TIMEOUT_IN_S = 3 * 60; + + static class TestAsyncContext { + int _asyncCallSize; + CountDownLatch _countDownLatch; + int[] _returnCode; + MetaClientInterface.Stat[] _stats; + String[] _data; + + TestAsyncContext(int callSize) { + _asyncCallSize = callSize; + _countDownLatch = new CountDownLatch(callSize); + _returnCode = new int[callSize]; + _stats = new MetaClientInterface.Stat[callSize]; + _data = new String[callSize]; + } + + public CountDownLatch getCountDownLatch() { + return _countDownLatch; + } + + public void countDown() { + _countDownLatch.countDown(); + } + + public int getReturnCode(int idx) { + return _returnCode[idx]; + } + + public MetaClientInterface.Stat getStats(int idx) { + return _stats[idx]; + } + + public String getData(int idx) { + return _data[idx]; + } + + public void setReturnCodeWhenFinished(int idx, int returnCode) { + _returnCode[idx] = returnCode; + } + + public void setStatWhenFinished(int idx, MetaClientInterface.Stat stat) { + _stats[idx] = stat; + } + + public void setDataWhenFinished(int idx, String data) { + _data[idx] = data; + } + } + + @Test + public void testAsyncCreateSetAndGet() { + asyncContext[0] = new TestAsyncContext(2); + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + + zkMetaClient + .asyncCreate(entryKey, "async_create-data", MetaClientInterface.EntryMode.PERSISTENT, + new AsyncCallback.VoidCallback() { + @Override + public void processResult(int returnCode, String key) { + asyncContext[0].setReturnCodeWhenFinished(0, returnCode); + asyncContext[0].countDown(); + } + }); + + zkMetaClient.asyncCreate(nonExistsEntry, "async_create-data-invalid", + MetaClientInterface.EntryMode.PERSISTENT, new AsyncCallback.VoidCallback() { + @Override + public void processResult(int returnCode, String key) { + asyncContext[0].setReturnCodeWhenFinished(1, returnCode); + asyncContext[0].countDown(); + } + }); + + asyncContext[0].getCountDownLatch().await(LATCH_WAIT_TIMEOUT_IN_S, TimeUnit.SECONDS); + + Assert.assertEquals(asyncContext[0].getReturnCode(0), KeeperException.Code.OK.intValue()); + Assert.assertEquals(asyncContext[0].getReturnCode(1), KeeperException.Code.NONODE.intValue()); + + // create the entry again and expect a duplicated error code + asyncContext[0] = new TestAsyncContext(1); + zkMetaClient + .asyncCreate(entryKey, "async_create-data", MetaClientInterface.EntryMode.PERSISTENT, + new AsyncCallback.VoidCallback() { + @Override + public void processResult(int returnCode, String key) { + asyncContext[0].setReturnCodeWhenFinished(0, returnCode); + asyncContext[0].countDown(); + } + }); + asyncContext[0].getCountDownLatch().await(LATCH_WAIT_TIMEOUT_IN_S, TimeUnit.SECONDS); + Assert.assertEquals(asyncContext[0].getReturnCode(0), + KeeperException.Code.NODEEXISTS.intValue()); + + + // test set + asyncContext[0] = new TestAsyncContext(1); + zkMetaClient + .asyncSet(entryKey, "async_create-data-new", 0, + new AsyncCallback.StatCallback() { + @Override + public void processResult(int returnCode, String key, + @Nullable MetaClientInterface.Stat stat) { + asyncContext[0].setReturnCodeWhenFinished(0, returnCode); + asyncContext[0].setStatWhenFinished(0, stat); + asyncContext[0].countDown(); + } + }); + asyncContext[0].getCountDownLatch().await(LATCH_WAIT_TIMEOUT_IN_S, TimeUnit.SECONDS); + Assert.assertEquals(asyncContext[0].getReturnCode(0), + KeeperException.Code.OK.intValue()); + Assert.assertEquals(asyncContext[0].getStats(0).getEntryType(), + MetaClientInterface.EntryMode.PERSISTENT); + Assert.assertEquals(asyncContext[0].getStats(0).getVersion(), 1); + + // test get + asyncContext[0] = new TestAsyncContext(1); + zkMetaClient.asyncGet(entryKey, new AsyncCallback.DataCallback() { + @Override + public void processResult(int returnCode, String key, byte[] data, + MetaClientInterface.Stat stat) { + asyncContext[0].setReturnCodeWhenFinished(0, returnCode); + asyncContext[0].setStatWhenFinished(0, stat); + asyncContext[0].setDataWhenFinished(0, zkMetaClient.deserialize(data, key)); + asyncContext[0].countDown(); + } + }); + + asyncContext[0].getCountDownLatch().await(LATCH_WAIT_TIMEOUT_IN_S, TimeUnit.SECONDS); + + Assert.assertEquals(asyncContext[0].getReturnCode(0), KeeperException.Code.OK.intValue()); + Assert.assertEquals(asyncContext[0].getStats(0).getEntryType(), + MetaClientInterface.EntryMode.PERSISTENT); + Assert.assertEquals(asyncContext[0].getStats(0).getVersion(), 1); + Assert.assertEquals(asyncContext[0].getData(0), "async_create-data-new"); + } catch (Exception ex) { + Assert.fail("Test testAsyncCreate failed because of:", ex); + } + } + + @Test(dependsOnMethods = "testAsyncCreateSetAndGet") + public void testAsyncExistsAndDelete() { + asyncContext[0] = new TestAsyncContext(2); + try (ZkMetaClient zkMetaClient = createZkMetaClient()) { + zkMetaClient.connect(); + + zkMetaClient.asyncExist(entryKey, new AsyncCallback.StatCallback() { + @Override + public void processResult(int returnCode, String key, MetaClientInterface.Stat stat) { + asyncContext[0].setReturnCodeWhenFinished(0, returnCode); + asyncContext[0].setStatWhenFinished(0, stat); + asyncContext[0].countDown(); + } + }); + + zkMetaClient.asyncExist(nonExistsEntry, new AsyncCallback.StatCallback() { + @Override + public void processResult(int returnCode, String key, MetaClientInterface.Stat stat) { + asyncContext[0].setReturnCodeWhenFinished(1, returnCode); + asyncContext[0].setStatWhenFinished(1, stat); + asyncContext[0].countDown(); + } + }); + + asyncContext[0].getCountDownLatch().await(LATCH_WAIT_TIMEOUT_IN_S, TimeUnit.SECONDS); + + Assert.assertEquals(asyncContext[0].getReturnCode(0), KeeperException.Code.OK.intValue()); + Assert.assertEquals(asyncContext[0].getStats(0).getEntryType(), + MetaClientInterface.EntryMode.PERSISTENT); + Assert.assertEquals(asyncContext[0].getStats(0).getVersion(), 1); + Assert.assertEquals(asyncContext[0].getReturnCode(1), KeeperException.Code.NONODE.intValue()); + Assert.assertNull(asyncContext[0].getStats(1)); + + // test delete + asyncContext[0] = new TestAsyncContext(1); + zkMetaClient.asyncDelete(entryKey, new AsyncCallback.VoidCallback() { + @Override + public void processResult(int returnCode, String key) { + asyncContext[0].setReturnCodeWhenFinished(0, returnCode); + asyncContext[0].countDown(); + } + }); + + asyncContext[0].getCountDownLatch().await(LATCH_WAIT_TIMEOUT_IN_S, TimeUnit.SECONDS); + + Assert.assertEquals(asyncContext[0].getReturnCode(0), KeeperException.Code.OK.intValue()); + + // node should not be there + Assert.assertNull(zkMetaClient.get(entryKey)); + } catch (InterruptedException ex) { + Assert.fail("Test testAsyncCreate failed because of:", ex); + } + } +} diff --git a/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/ZkMetaClientTestBase.java b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/ZkMetaClientTestBase.java new file mode 100644 index 0000000000..2a5f4b97bb --- /dev/null +++ b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/ZkMetaClientTestBase.java @@ -0,0 +1,93 @@ +package org.apache.helix.metaclient.impl.zk; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.apache.helix.metaclient.impl.zk.factory.ZkMetaClientConfig; +import org.apache.helix.zookeeper.zkclient.IDefaultNameSpace; +import org.apache.helix.zookeeper.zkclient.ZkServer; +import org.testng.annotations.AfterSuite; +import org.testng.annotations.BeforeSuite; + + +public abstract class ZkMetaClientTestBase { + + protected static final String ZK_ADDR = "localhost:2183"; + protected static final int DEFAULT_TIMEOUT_MS = 1000; + protected static final String ENTRY_STRING_VALUE = "test-value"; + private static ZkServer _zkServer; + + /** + * Creates local Zk Server + * Note: Cannot test container / TTL node end to end behavior as + * the zk server setup doesn't allow for that. To enable this, zk server + * setup must invoke ContainerManager.java. However, the actual + * behavior has been verified to work on native ZK Client. + * TODO: Modify zk server setup to include ContainerManager. + * This can be done through ZooKeeperServerMain.java or + * LeaderZooKeeperServer.java. + */ + @BeforeSuite + public void prepare() { + // Enable extended types and create a ZkClient + System.setProperty("zookeeper.extendedTypesEnabled", "true"); + // start local zookeeper server + _zkServer = startZkServer(ZK_ADDR); + } + + @AfterSuite + public void cleanUp() { + _zkServer.shutdown(); + } + + protected static ZkMetaClient createZkMetaClient() { + ZkMetaClientConfig config = + new ZkMetaClientConfig.ZkMetaClientConfigBuilder().setConnectionAddress(ZK_ADDR) + //.setZkSerializer(new TestStringSerializer()) + .build(); + return new ZkMetaClient<>(config); + } + + public static ZkServer startZkServer(final String zkAddress) { + String zkDir = zkAddress.replace(':', '_'); + final String logDir = "/tmp/" + zkDir + "/logs"; + final String dataDir = "/tmp/" + zkDir + "/dataDir"; + + // Clean up local directory + try { + FileUtils.deleteDirectory(new File(dataDir)); + FileUtils.deleteDirectory(new File(logDir)); + } catch (IOException e) { + e.printStackTrace(); + } + + IDefaultNameSpace defaultNameSpace = zkClient -> { + }; + + int port = Integer.parseInt(zkAddress.substring(zkAddress.lastIndexOf(':') + 1)); + System.out.println("Starting ZK server at " + zkAddress); + ZkServer zkServer = new ZkServer(dataDir, logDir, defaultNameSpace, port); + zkServer.start(); + return zkServer; + } +} diff --git a/meta-client/src/test/java/org/apache/helix/metaclient/recipes/lock/LockClientTest.java b/meta-client/src/test/java/org/apache/helix/metaclient/recipes/lock/LockClientTest.java new file mode 100644 index 0000000000..a8ca387cad --- /dev/null +++ b/meta-client/src/test/java/org/apache/helix/metaclient/recipes/lock/LockClientTest.java @@ -0,0 +1,110 @@ +package org.apache.helix.metaclient.recipes.lock; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.helix.metaclient.api.MetaClientInterface; +import org.apache.helix.metaclient.factories.MetaClientConfig; +import org.apache.helix.metaclient.impl.zk.ZkMetaClientTestBase; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class LockClientTest extends ZkMetaClientTestBase { + + private static final String TEST_INVALID_PATH = "/_invalid/a/b/c"; + + public LockClient createLockClient() { + + MetaClientConfig.StoreType storeType = MetaClientConfig.StoreType.ZOOKEEPER; + MetaClientConfig config = new MetaClientConfig.MetaClientConfigBuilder<>().setConnectionAddress(ZK_ADDR) + .setStoreType(storeType).build(); + return new LockClient(config); + } + + @Test + public void testAcquireLock() { + final String key = "/TestLockClient_testAcquireLock"; + LockClient lockClient = createLockClient(); + LockInfo lockInfo = new LockInfo(); + lockClient.acquireLock(key, lockInfo, MetaClientInterface.EntryMode.PERSISTENT); + Assert.assertNotNull(lockClient.retrieveLock(key)); + try { + lockClient.acquireLock(TEST_INVALID_PATH, new LockInfo(), MetaClientInterface.EntryMode.PERSISTENT); + Assert.fail("Should not be able to acquire lock for key: " + key); + } catch (Exception e) { + // expected + } + } + + @Test + public void testReleaseLock() { + final String key = "/TestLockClient_testReleaseLock"; + LockClient lockClient = createLockClient(); + LockInfo lockInfo = new LockInfo(); + lockClient.acquireLock(key, lockInfo, MetaClientInterface.EntryMode.PERSISTENT); + Assert.assertNotNull(lockClient.retrieveLock(key)); + + lockClient.releaseLock(key); + Assert.assertNull(lockClient.retrieveLock(key)); + lockClient.releaseLock(TEST_INVALID_PATH); + } + + @Test + public void testAcquireTTLLock() { + final String key = "/TestLockClient_testAcquireTTLLock"; + LockClient lockClient = createLockClient(); + LockInfo lockInfo = new LockInfo(); + lockClient.acquireLockWithTTL(key, lockInfo, 1L); + Assert.assertNotNull(lockClient.retrieveLock(key)); + try { + lockClient.acquireLockWithTTL(TEST_INVALID_PATH, lockInfo, 1L); + Assert.fail("Should not be able to acquire lock for key: " + key); + } catch (Exception e) { + // expected + } + } + + @Test + public void testRetrieveLock() { + final String key = "/TestLockClient_testRetrieveLock"; + LockClient lockClient = createLockClient(); + LockInfo lockInfo = new LockInfo(); + lockClient.acquireLock(key, lockInfo, MetaClientInterface.EntryMode.PERSISTENT); + Assert.assertNotNull(lockClient.retrieveLock(key)); + Assert.assertNull(lockClient.retrieveLock(TEST_INVALID_PATH)); + } + + @Test + public void testRenewTTLLock() { + final String key = "/TestLockClient_testRenewTTLLock"; + LockClient lockClient = createLockClient(); + LockInfo lockInfo = new LockInfo(); + lockClient.acquireLockWithTTL(key, lockInfo, 1L); + Assert.assertNotNull(lockClient.retrieveLock(key)); + + lockClient.renewTTLLock(key); + Assert.assertNotSame(lockClient.retrieveLock(key).getGrantedAt(), lockInfo.getLastRenewedAt()); + try { + lockClient.renewTTLLock(TEST_INVALID_PATH); + Assert.fail("Should not be able to renew lock for key: " + key); + } catch (Exception e) { + // expected + } + } +} diff --git a/meta-client/src/test/java/org/apache/helix/metaclient/recipes/lock/LockInfoTest.java b/meta-client/src/test/java/org/apache/helix/metaclient/recipes/lock/LockInfoTest.java new file mode 100644 index 0000000000..4f48a5a0a6 --- /dev/null +++ b/meta-client/src/test/java/org/apache/helix/metaclient/recipes/lock/LockInfoTest.java @@ -0,0 +1,70 @@ +package org.apache.helix.metaclient.recipes.lock; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +import java.time.Duration; + +import org.apache.helix.metaclient.datamodel.DataRecord; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class LockInfoTest { + private static final String OWNER_ID = "urn:li:principal:UNKNOWN"; + private static final String CLIENT_ID = "test_client_id"; + private static final String CLIENT_DATA = "client_data"; + private static final String LOCK_ID = "794c8a4c-c14b-4c23-b83f-4e1147fc6978"; + private static final long GRANT_TIME = System.currentTimeMillis(); + private static final long LAST_RENEWAL_TIME = System.currentTimeMillis(); + private static final long TIMEOUT = 100000; + + public static final String DEFAULT_LOCK_ID_TEXT = ""; + public static final String DEFAULT_OWNER_ID_TEXT = ""; + public static final String DEFAULT_CLIENT_ID_TEXT = ""; + public static final String DEFAULT_CLIENT_DATA = ""; + public static final long DEFAULT_GRANTED_AT_LONG = -1L; + public static final long DEFAULT_LAST_RENEWED_AT_LONG = -1L; + public static final long DEFAULT_TIMEOUT_DURATION = -1L; + + @Test + public void testLockInfo() { + LockInfo lockInfo = + new LockInfo(LOCK_ID, OWNER_ID, CLIENT_ID, CLIENT_DATA, GRANT_TIME, + LAST_RENEWAL_TIME, TIMEOUT); + + Assert.assertEquals(LOCK_ID, lockInfo.getLockId()); + Assert.assertEquals(OWNER_ID, lockInfo.getOwnerId()); + Assert.assertEquals(CLIENT_ID, lockInfo.getClientId()); + Assert.assertEquals(CLIENT_DATA, lockInfo.getClientData()); + Assert.assertEquals(GRANT_TIME, (long) lockInfo.getGrantedAt()); + Assert.assertEquals(LAST_RENEWAL_TIME, (long) lockInfo.getLastRenewedAt()); + Assert.assertEquals(TIMEOUT, lockInfo.getTimeout()); + + DataRecord dataRecord = new DataRecord("dataRecord"); + LockInfo lockInfo1 = new LockInfo(dataRecord); + Assert.assertEquals(DEFAULT_LOCK_ID_TEXT, lockInfo1.getLockId()); + Assert.assertEquals(DEFAULT_OWNER_ID_TEXT, lockInfo1.getOwnerId()); + Assert.assertEquals(DEFAULT_CLIENT_ID_TEXT, lockInfo1.getClientId()); + Assert.assertEquals(DEFAULT_CLIENT_DATA, lockInfo1.getClientData()); + Assert.assertEquals(DEFAULT_GRANTED_AT_LONG, (long) lockInfo1.getGrantedAt()); + Assert.assertEquals(DEFAULT_LAST_RENEWED_AT_LONG, (long) lockInfo1.getLastRenewedAt()); + Assert.assertEquals(DEFAULT_TIMEOUT_DURATION, lockInfo1.getTimeout()); + } +} diff --git a/pom.xml b/pom.xml index cad78a39fa..6318127720 100644 --- a/pom.xml +++ b/pom.xml @@ -310,6 +310,7 @@ helix-front recipes helix-view-aggregator + meta-client diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/ZkClient.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/ZkClient.java index 915a607779..1dd601c21e 100644 --- a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/ZkClient.java +++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/ZkClient.java @@ -87,13 +87,23 @@ public class ZkClient extends org.apache.helix.zookeeper.zkclient.ZkClient imple * The JMX bean name will be: HelixZkClient.monitorType.monitorKey.monitorInstanceName. * @param monitorRootPathOnly * Should only stat of access to root path be reported to JMX bean or path-specific stat be reported too. + * @param connectOnInit true if connect to ZK during initialization, otherwise user will need to call connect + * explicitly before talking to ZK. */ + public ZkClient(IZkConnection zkConnection, int connectionTimeout, long operationRetryTimeout, + PathBasedZkSerializer zkSerializer, String monitorType, String monitorKey, + String monitorInstanceName, boolean monitorRootPathOnly, boolean connectOnInit, + boolean usePersistWatcher) { + super(zkConnection, connectionTimeout, operationRetryTimeout, zkSerializer, monitorType, + monitorKey, monitorInstanceName, monitorRootPathOnly, connectOnInit, usePersistWatcher); + } + public ZkClient(IZkConnection zkConnection, int connectionTimeout, long operationRetryTimeout, PathBasedZkSerializer zkSerializer, String monitorType, String monitorKey, String monitorInstanceName, boolean monitorRootPathOnly) { - super(zkConnection, connectionTimeout, operationRetryTimeout, zkSerializer, monitorType, - monitorKey, monitorInstanceName, monitorRootPathOnly); + this(zkConnection, connectionTimeout, operationRetryTimeout, zkSerializer, monitorType, + monitorKey, monitorInstanceName, monitorRootPathOnly, true, false); } public ZkClient(IZkConnection connection, int connectionTimeout, @@ -189,6 +199,17 @@ public static class Builder { String _monitorKey; String _monitorInstanceName = null; boolean _monitorRootPathOnly = true; + boolean _connectOnInit = true; + boolean _usePersistWatcher = false; + + /** + * If set true, the client will connect to ZK during initialization. + * Otherwise, user has to call connect() method explicitly before talking to ZK. + */ + public Builder setConnectOnInit(boolean connectOnInit) { + _connectOnInit = connectOnInit; + return this; + } public Builder setConnection(IZkConnection connection) { this._connection = connection; @@ -258,6 +279,11 @@ public Builder setOperationRetryTimeout(Long operationRetryTimeout) { return this; } + public Builder setUsePersistWatcher(boolean usePersistWatcher) { + this._usePersistWatcher = usePersistWatcher; + return this; + } + public ZkClient build() { if (_connection == null) { if (_zkServer == null) { @@ -273,7 +299,8 @@ public ZkClient build() { } return new ZkClient(_connection, _connectionTimeout, _operationRetryTimeout, _zkSerializer, - _monitorType, _monitorKey, _monitorInstanceName, _monitorRootPathOnly); + _monitorType, _monitorKey, _monitorInstanceName, _monitorRootPathOnly, _connectOnInit, + _usePersistWatcher); } } } diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkChildListener.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkChildListener.java index 7623a2ec17..a150f16430 100644 --- a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkChildListener.java +++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkChildListener.java @@ -20,6 +20,8 @@ */ import java.util.List; +import org.apache.zookeeper.Watcher; + /** * An {@link IZkChildListener} can be registered at a {@link ZkClient} for listening on zk child changes for a given @@ -42,4 +44,17 @@ public interface IZkChildListener { * @throws Exception */ public void handleChildChange(String parentPath, List currentChilds) throws Exception; + + /** + * Called when the children of the given path changed. + * + * @param parentPath The parent path + * @param currentChilds The children or null if the root node (parent path) was deleted. + * @param eventType The zookeeper event type + * @throws Exception + */ + default void handleChildChange(String parentPath, List currentChilds, Watcher.Event.EventType eventType) + throws Exception { + handleChildChange(parentPath, currentChilds); + } } diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkConnection.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkConnection.java index e766bf7d97..43eefad26a 100644 --- a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkConnection.java +++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkConnection.java @@ -21,6 +21,7 @@ import java.util.List; +import org.apache.zookeeper.AddWatchMode; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.Op; @@ -63,4 +64,7 @@ public interface IZkConnection { public List multi(Iterable ops) throws KeeperException, InterruptedException; public void addAuthInfo(String scheme, byte[] auth); + public void addWatch(String basePath, Watcher watcher, AddWatchMode mode) + throws KeeperException, InterruptedException; + public void removeWatches(String path, Watcher watcher, Watcher.WatcherType watcherType) throws InterruptedException, KeeperException; } \ No newline at end of file diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkDataListener.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkDataListener.java index 6d90e8de81..0a3e8b3e53 100644 --- a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkDataListener.java +++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkDataListener.java @@ -19,6 +19,9 @@ * under the License. */ +import org.apache.zookeeper.Watcher; + + /** * An {@link IZkDataListener} can be registered at a {@link ZkClient} for listening on zk data changes for a given path. * @@ -31,4 +34,8 @@ public interface IZkDataListener { public void handleDataChange(String dataPath, Object data) throws Exception; public void handleDataDeleted(String dataPath) throws Exception; + + default void handleDataChange(String dataPath, Object data, Watcher.Event.EventType eventType) throws Exception { + handleDataChange(dataPath, data); + } } diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkStateListener.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkStateListener.java index 5970e623d0..f2c4190de6 100644 --- a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkStateListener.java +++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkStateListener.java @@ -58,4 +58,8 @@ public interface IZkStateListener { * On any error. */ void handleSessionEstablishmentError(final Throwable error) throws Exception; + + default void handleStateChanged(KeeperState prevState, KeeperState curState) throws Exception { + handleStateChanged(curState); + } } diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/RecursivePersistListener.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/RecursivePersistListener.java new file mode 100644 index 0000000000..96c09b340c --- /dev/null +++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/RecursivePersistListener.java @@ -0,0 +1,41 @@ +package org.apache.helix.zookeeper.zkclient; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.zookeeper.Watcher; + +/** + * An {@link RecursivePersistListener} can be registered at a {@link ZkClient} for listening on all + * zk children changes in a tree structure for a given path. + * + * The listener is a persist listener. No need to resubscribe. + * + */ +public interface RecursivePersistListener { + /** + * invoked when there is a node added, removed or node data change in the tree structure of + * that RecursivePersistListener subscribed path + * @param dataPath The path of ZNode that change happened + * @param eventType Event type, including NodeCreated, NodeDataChanged and NodeDeleted + * @throws Exception + */ + public void handleZNodeChange(String dataPath, Watcher.Event.EventType eventType) + throws Exception; +} diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java index 78a3c88ed1..064f6b494d 100644 --- a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java +++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java @@ -32,10 +32,11 @@ import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Stream; import javax.management.JMException; import com.google.common.annotations.VisibleForTesting; - import org.apache.helix.zookeeper.api.client.ChildrenSubscribeResult; import org.apache.helix.zookeeper.constant.ZkSystemPropertyKeys; import org.apache.helix.zookeeper.datamodel.SessionAwareZNRecord; @@ -61,6 +62,8 @@ import org.apache.helix.zookeeper.zkclient.serialize.PathBasedZkSerializer; import org.apache.helix.zookeeper.zkclient.serialize.ZkSerializer; import org.apache.helix.zookeeper.zkclient.util.ExponentialBackoffStrategy; +import org.apache.helix.zookeeper.zkclient.util.ZkPathRecursiveWatcherTrie; +import org.apache.zookeeper.AddWatchMode; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.KeeperException.ConnectionLossException; @@ -78,6 +81,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; + /** * "Native ZkClient": not to be used directly. * @@ -104,7 +108,7 @@ public class ZkClient implements Watcher { private static AtomicLong UID = new AtomicLong(0); public final long _uid; - // ZNode write size limit in bytes. + // ZNode write size limit in bytes: // TODO: use ZKConfig#JUTE_MAXBUFFER once bumping up ZK to 3.5.2+ private static final int WRITE_SIZE_LIMIT = Integer.getInteger(ZkSystemPropertyKeys.JUTE_MAXBUFFER, ZNRecord.SIZE_LIMIT); @@ -114,6 +118,8 @@ public class ZkClient implements Watcher { private final Map> _childListener = new ConcurrentHashMap<>(); private final ConcurrentHashMap> _dataListener = new ConcurrentHashMap<>(); + private final ZkPathRecursiveWatcherTrie _zkPathRecursiveWatcherTrie = + new ZkPathRecursiveWatcherTrie(); private final Set _stateListener = new CopyOnWriteArraySet<>(); private KeeperState _currentState; private final ZkLock _zkEventLock = new ZkLock(); @@ -134,6 +140,13 @@ public class ZkClient implements Watcher { private volatile boolean _closed; private PathBasedZkSerializer _pathBasedZkSerializer; private ZkClientMonitor _monitor; + // When _usePersistWatcher is true, ZKClient will register itself as persist watcher to Zk Server. + // No re-register is needed after change event. + // Default value is false, meaning ZKClient will register itself as regular ont time watcher to + // Zk Server and will re-register after an data or child change. + private boolean _usePersistWatcher; + + private final ReentrantLock _persistListenerMutex; // To automatically retry the async operation, we need a separate thread other than the // ZkEventThread. Otherwise the retry request might block the normal event processing. @@ -217,7 +230,7 @@ public void recordPathStat(Stat stat, OptionalLong notificationTime) { protected ZkClient(IZkConnection zkConnection, int connectionTimeout, long operationRetryTimeout, PathBasedZkSerializer zkSerializer, String monitorType, String monitorKey, - String monitorInstanceName, boolean monitorRootPathOnly) { + String monitorInstanceName, boolean monitorRootPathOnly, boolean connectOnInit, boolean usePersistWatcher) { if (zkConnection == null) { throw new NullPointerException("Zookeeper connection is null!"); } @@ -243,15 +256,18 @@ protected ZkClient(IZkConnection zkConnection, int connectionTimeout, long opera LOG.info("ZkClient monitor key or type is not provided. Skip monitoring."); } - connect(connectionTimeout, this); - - try { - if (_monitor != null) { - _monitor.register(); - } - } catch (JMException e){ - LOG.error("Error in creating ZkClientMonitor", e); + if (connectOnInit) { + connect(connectionTimeout, this); } + _usePersistWatcher = usePersistWatcher; + _persistListenerMutex = new ReentrantLock(); + } + + protected ZkClient(IZkConnection zkConnection, int connectionTimeout, long operationRetryTimeout, + PathBasedZkSerializer zkSerializer, String monitorType, String monitorKey, + String monitorInstanceName, boolean monitorRootPathOnly) { + this(zkConnection, connectionTimeout, operationRetryTimeout, zkSerializer, monitorType, monitorKey, + monitorInstanceName, monitorRootPathOnly, true, false); } public List subscribeChildChanges(String path, IZkChildListener listener) { @@ -260,19 +276,20 @@ public List subscribeChildChanges(String path, IZkChildListener listener } public ChildrenSubscribeResult subscribeChildChanges(String path, IZkChildListener listener, boolean skipWatchingNonExistNode) { - synchronized (_childListener) { - Set listeners = _childListener.get(path); - if (listeners == null) { - listeners = new CopyOnWriteArraySet<>(); - _childListener.put(path, listeners); + if (_usePersistWatcher) { + addPersistListener(path, listener); + } else { + synchronized (_childListener) { + addChildListener(path, listener); } - listeners.add(listener); } List children = watchForChilds(path, skipWatchingNonExistNode); if (children == null && skipWatchingNonExistNode) { unsubscribeChildChanges(path, listener); - LOG.info("zkclient{}, watchForChilds failed to install no-existing watch and add listener. Path: {}", _uid, path); + LOG.info( + "zkclient{}, watchForChilds failed to install no-existing watch and add listener. Path: {}", + _uid, path); return new ChildrenSubscribeResult(children, false); } @@ -280,29 +297,21 @@ public ChildrenSubscribeResult subscribeChildChanges(String path, IZkChildListen } public void unsubscribeChildChanges(String path, IZkChildListener childListener) { - synchronized (_childListener) { - final Set listeners = _childListener.get(path); - if (listeners != null) { - listeners.remove(childListener); + if (_usePersistWatcher) { + removePersistListener(path, childListener); + } else { + synchronized (_childListener) { + removeChildListener(path, childListener); } } } public boolean subscribeDataChanges(String path, IZkDataListener listener, boolean skipWatchingNonExistNode) { - Set listenerEntries; - synchronized (_dataListener) { - listenerEntries = _dataListener.get(path); - if (listenerEntries == null) { - listenerEntries = new CopyOnWriteArraySet<>(); - _dataListener.put(path, listenerEntries); - } - - boolean prefetchEnabled = isPrefetchEnabled(listener); - IZkDataListenerEntry listenerEntry = new IZkDataListenerEntry(listener, prefetchEnabled); - listenerEntries.add(listenerEntry); - if (prefetchEnabled) { - LOG.debug("zkclient {} subscribed data changes for {}, listener {}, prefetch data {}", - _uid, path, listener, prefetchEnabled); + if (_usePersistWatcher) { + addPersistListener(path, listener); + } else { + synchronized (_dataListener) { + addDataListener(path, listener); } } @@ -310,7 +319,8 @@ public boolean subscribeDataChanges(String path, IZkDataListener listener, boole if (!watchInstalled) { // Now let us remove this handler. unsubscribeDataChanges(path, listener); - LOG.info("zkclient {} watchForData failed to install no-existing path and thus add listener. Path: {}", + LOG.info( + "zkclient {} watchForData failed to install no-existing path and thus add listener. Path: {}", _uid, path); return false; } @@ -319,16 +329,70 @@ public boolean subscribeDataChanges(String path, IZkDataListener listener, boole return true; } - /** - * Subscribe the path and the listener will handle data events of the path - * WARNING: if the path is created after deletion, users need to re-subscribe the path - * @param path The zookeeper path - * @param listener Instance of {@link IZkDataListener} - */ + /** + * Subscribe the path and the listener will handle data events of the path + * WARNING: if the path is created after deletion, users need to re-subscribe the path + * @param path The zookeeper path + * @param listener Instance of {@link IZkDataListener} + */ public void subscribeDataChanges(String path, IZkDataListener listener) { subscribeDataChanges(path, listener, false); } + /** + * Subscribe RecursivePersistListener for a particular path. User can only subscribe when + * `_usePersistWatcher` is set to true and there is no pre-existing watcher on the path. + */ + public void subscribePersistRecursiveListener(String path, + RecursivePersistListener recursivePersistListener) { + if (!_usePersistWatcher) { + throw new UnsupportedOperationException( + "Can not subscribe PersistRecursiveWatcher. Persist listener is not enabled."); + } + + ManipulateListener addListener = () -> { + if (hasChildOrDataListeners(path)) { + throw new UnsupportedOperationException( + "Can not subscribe PersistRecursiveWatcher. There is an existing listener on " + path); + } + // subscribe a PERSISTENT_RECURSIVE listener on path. It throws exception if not successful + retryUntilConnected(() -> { + getConnection().addWatch(path, ZkClient.this, AddWatchMode.PERSISTENT_RECURSIVE); + return null; + }); + + _zkPathRecursiveWatcherTrie.addRecursiveListener(path, recursivePersistListener); + }; + + executeWithInPersistListenerMutex(addListener); + } + + public void unsubscribePersistRecursiveListener(String path, + RecursivePersistListener recursivePersistListener) { + if (!_usePersistWatcher) { + throw new UnsupportedOperationException( + "Can not subscribe PersistRecursiveWatcher. Persist listener is not enabled."); + } + + // unsubscribe from ZK if this is the only recursive persist listener on this path. + ManipulateListener removeListeners = () -> { + _zkPathRecursiveWatcherTrie.removeRecursiveListener(path, recursivePersistListener); + if (_zkPathRecursiveWatcherTrie.hasListenerOnPath(path)) { + return; + } + try { + // We are not checking if there is a persist listener registered on the path because + // we do not allow subscribe a persist listener on the same path of persist recursive + // listener as of now. + getConnection().removeWatches(path, this, WatcherType.Any); + } catch (KeeperException.NoWatcherException e) { + LOG.warn("Persist watcher is already removed on path: {}", path); + } + }; + executeWithInPersistListenerMutex(removeListeners); + + } + private boolean isPrefetchEnabled(IZkDataListener dataListener) { PreFetchChangedData preFetch = dataListener.getClass().getAnnotation(PreFetchChangedData.class); if (preFetch != null) { @@ -352,14 +416,11 @@ private boolean isPrefetchEnabled(IZkDataListener dataListener) { } public void unsubscribeDataChanges(String path, IZkDataListener dataListener) { - synchronized (_dataListener) { - final Set listeners = _dataListener.get(path); - if (listeners != null) { - IZkDataListenerEntry listenerEntry = new IZkDataListenerEntry(dataListener); - listeners.remove(listenerEntry); - } - if (listeners == null || listeners.isEmpty()) { - _dataListener.remove(path); + if (_usePersistWatcher) { + removePersistListener(path, dataListener); + } else { + synchronized (_dataListener) { + removeDataListener(path, dataListener); } } } @@ -407,6 +468,20 @@ public void unsubscribeStateChanges( } public void unsubscribeAll() { + if (_usePersistWatcher) { + ManipulateListener removeAllListeners = () -> { + Stream.concat(_childListener.keySet().stream(), _dataListener.keySet().stream()) + .forEach(p -> { + try { + getConnection().removeWatches(p, this, WatcherType.Any); + } catch (InterruptedException | KeeperException e) { + LOG.error("Failed to remove persistent watcher for {} ", p, e); + } + }); + }; + executeWithInPersistListenerMutex(removeAllListeners); + return; + } synchronized (_childListener) { _childListener.clear(); } @@ -1250,6 +1325,9 @@ public void process(WatchedEvent event) { long notificationTime = System.currentTimeMillis(); LOG.debug("zkclient {}, Received event: {} ", _uid, event); + if (event.getType() == EventType.PersistentWatchRemoved) { + return; + } _zookeeperEventThread = Thread.currentThread(); boolean stateChanged = event.getPath() == null; @@ -1306,13 +1384,13 @@ public void process(WatchedEvent event) { } } - private void fireAllEvents() { + private void fireAllEvents(WatchedEvent event) { //TODO: During handling new session, if the path is deleted, watcher leakage could still happen for (Entry> entry : _childListener.entrySet()) { - fireChildChangedEvents(entry.getKey(), entry.getValue(), true); + fireChildChangedEvents(entry.getKey(), entry.getValue(), true, event.getType()); } for (Entry> entry : _dataListener.entrySet()) { - fireDataChangedEvents(entry.getKey(), entry.getValue(), OptionalLong.empty(), true); + fireDataChangedEvents(entry.getKey(), entry.getValue(), OptionalLong.empty(), true, event.getType()); } } @@ -1331,10 +1409,11 @@ private void fireAllEvents() { * are deleted before the last page is fetched. The upstream caller should be able to handle this. */ public List getChildren(String path) { - return getChildren(path, hasListeners(path)); + return getChildren(path, (!_usePersistWatcher) && hasChildOrDataListeners(path)); } protected List getChildren(final String path, final boolean watch) { + validateNativeZkWatcherType(watch); long startT = System.currentTimeMillis(); try { @@ -1395,10 +1474,11 @@ public int countChildren(String path) { } public boolean exists(final String path) { - return exists(path, hasListeners(path)); + return exists(path, hasChildOrDataListeners(path)); } protected boolean exists(final String path, final boolean watch) { + validateNativeZkWatcherType(watch); long startT = System.currentTimeMillis(); try { boolean exists = retryUntilConnected(new Callable() { @@ -1428,6 +1508,7 @@ public Stat getStat(final String path) { } private Stat getStat(final String path, final boolean watch) { + validateNativeZkWatcherType(watch); long startT = System.currentTimeMillis(); final Stat stat; try { @@ -1478,12 +1559,13 @@ private Stat installWatchOnlyPathExist(final String path) { protected void processStateChanged(WatchedEvent event) { LOG.info("zkclient {}, zookeeper state changed ( {} )", _uid, event.getState()); + KeeperState prevState = _currentState; setCurrentState(event.getState()); if (getShutdownTrigger()) { return; } - fireStateChangedEvent(event.getState()); + fireStateChangedEvent(prevState, event.getState()); /* * Note, the intention is that only the ZkClient managing the session would do auto reconnect @@ -1512,7 +1594,7 @@ protected void processStateChanged(WatchedEvent event) { * reconnecting when the session expired. Because previous session expired, we also have to * notify all listeners that something might have changed. */ - fireAllEvents(); + fireAllEvents(event); } } else if (event.getState() == KeeperState.Expired) { _isNewSessionEventFired = false; @@ -1652,15 +1734,15 @@ public void run() throws Exception { } } - protected void fireStateChangedEvent(final KeeperState state) { + protected void fireStateChangedEvent(final KeeperState prevState, final KeeperState curState) { final String sessionId = getHexSessionId(); for (final IZkStateListener stateListener : _stateListener) { - final String description = "State changed to " + state + " sent to " + stateListener; + final String description = "State changed to " + curState + " sent to " + stateListener; _eventThread.send(new ZkEventThread.ZkEvent(description, sessionId) { @Override public void run() throws Exception { - stateListener.handleStateChanged(state); + stateListener.handleStateChanged(prevState, curState); } }); } @@ -1679,7 +1761,7 @@ public void run() throws Exception { } } - private boolean hasListeners(String path) { + private boolean hasChildOrDataListeners(String path) { Set dataListeners = _dataListener.get(path); if (dataListeners != null && dataListeners.size() > 0) { return true; @@ -1751,7 +1833,7 @@ private void processDataOrChildChange(WatchedEvent event, long notificationTime) if (childListeners != null && !childListeners.isEmpty()) { // TODO recording child changed event propagation latency as well. Note this change will // introduce additional ZK access. - fireChildChangedEvents(path, childListeners, pathExists); + fireChildChangedEvents(path, childListeners, pathExists, event.getType()); } } @@ -1760,13 +1842,30 @@ private void processDataOrChildChange(WatchedEvent event, long notificationTime) Set listeners = _dataListener.get(path); if (listeners != null && !listeners.isEmpty()) { fireDataChangedEvents(event.getPath(), listeners, OptionalLong.of(notificationTime), - pathExists); + pathExists, event.getType()); + } + + // fire change event for persist recursive listener + if (_usePersistWatcher) { + Set recListeners = + _zkPathRecursiveWatcherTrie.getAllRecursiveListeners(path); + if (!recListeners.isEmpty()) { + for (final RecursivePersistListener listener : recListeners) { + _eventThread.send( + new ZkEventThread.ZkEvent("Data of " + path + " changed sent to " + listener) { + @Override + public void run() throws Exception { + listener.handleZNodeChange(path, event.getType()); + } + }); + } + } } } } private void fireDataChangedEvents(final String path, Set listeners, - final OptionalLong notificationTime, boolean pathExists) { + final OptionalLong notificationTime, boolean pathExists, EventType eventType) { try { final ZkPathStatRecord pathStatRecord = new ZkPathStatRecord(path); // Trigger listener callbacks @@ -1785,7 +1884,8 @@ public void run() throws Exception { // the exists() useGetData (false) route to check stat. Otherwise, we use getData() // to install watch. Stat stat = null; - if (!pathExists) { + // no register one time watcher when _usePersistWatcher is true. + if (_usePersistWatcher || !pathExists) { stat = getStat(path, false); } else { stat = installWatchOnlyPathExist(path); @@ -1800,14 +1900,14 @@ public void run() throws Exception { LOG.debug("zkclient {} Prefetch data for path: {}", _uid, path); try { // TODO: the data is redundantly read multiple times when multiple listeners exist - data = readData(path, null, true); + data = readData(path, null, !_usePersistWatcher); } catch (ZkNoNodeException e) { LOG.warn("zkclient {} Prefetch data for path: {} failed.", _uid, path, e); listener.getDataListener().handleDataDeleted(path); return; } } - listener.getDataListener().handleDataChange(path, data); + listener.getDataListener().handleDataChange(path, data, eventType); } } }); @@ -1817,7 +1917,8 @@ public void run() throws Exception { } } - private void fireChildChangedEvents(final String path, Set childListeners, boolean pathExists) { + private void fireChildChangedEvents(final String path, Set childListeners, boolean pathExists, + EventType eventType) { try { final ZkPathStatRecord pathStatRecord = new ZkPathStatRecord(path); for (final IZkChildListener listener : childListeners) { @@ -1826,7 +1927,7 @@ private void fireChildChangedEvents(final String path, Set chi public void run() throws Exception { if (!pathStatRecord.pathChecked()) { Stat stat = null; - if (!pathExists || !hasListeners(path)) { + if (_usePersistWatcher || !pathExists || !hasChildOrDataListeners(path)) { // will not install listener using exists call stat = getStat(path, false); } else { @@ -1844,7 +1945,7 @@ public void run() throws Exception { // Continue trigger the change handler } } - listener.handleChildChange(path, children); + listener.handleChildChange(path, children, eventType); } }); } @@ -2131,11 +2232,12 @@ public T readData(String path, boolean returnNullIfPathNotExi @SuppressWarnings("unchecked") public T readData(String path, Stat stat) { - return (T) readData(path, stat, hasListeners(path)); + return (T) readData(path, stat, hasChildOrDataListeners(path)); } @SuppressWarnings("unchecked") public T readData(final String path, final Stat stat, final boolean watch) { + validateNativeZkWatcherType(watch); long startT = System.currentTimeMillis(); byte[] data = null; try { @@ -2407,10 +2509,25 @@ public void watchForData(final String path) { private boolean watchForData(final String path, boolean skipWatchingNonExistNode) { try { - if (skipWatchingNonExistNode) { - retryUntilConnected(() -> (((ZkConnection) getConnection()).getZookeeper().getData(path, true, new Stat()))); + if (_usePersistWatcher) { + return retryUntilConnected(new Callable() { + @Override + public Boolean call() throws Exception { + if (!skipWatchingNonExistNode || exists(path)) { + getConnection().addWatch(path, ZkClient.this, AddWatchMode.PERSISTENT); + return true; + } + return false; + } + }); } else { - retryUntilConnected(() -> (((ZkConnection) getConnection()).getZookeeper().exists(path, true))); + if (skipWatchingNonExistNode) { + retryUntilConnected(() -> (((ZkConnection) getConnection()).getZookeeper() + .getData(path, true, new Stat()))); + } else { + retryUntilConnected( + () -> (((ZkConnection) getConnection()).getZookeeper().exists(path, true))); + } } } catch (ZkNoNodeException e) { // Do nothing, this is what we want as this is not going to leak watch in ZooKeeepr server. @@ -2462,11 +2579,20 @@ private List watchForChilds(final String path, boolean skipWatchingNonEx return retryUntilConnected(new Callable>() { @Override public List call() throws Exception { - if (!skipWatchingNonExistNode) { + // We only register one time watcher without checking in path exists + // when _usePersistWatcher is false and skipWatchingNonExistNode is false. + if (!skipWatchingNonExistNode && !_usePersistWatcher) { exists(path, true); } try { - return getChildren(path, true); + if (_usePersistWatcher) { + if (!skipWatchingNonExistNode || exists(path)) { + getConnection().addWatch(path, ZkClient.this, AddWatchMode.PERSISTENT); + } + return getChildren(path, false); + } else { + return getChildren(path, true); + } } catch (ZkNoNodeException e) { // ignore, the "exists" watch will listen for the parent node to appear LOG.info("zkclient{} watchForChilds path not existing:{} skipWatchingNodeNoteExist: {}", @@ -2494,6 +2620,11 @@ public Object call() throws Exception { }); } + public void connect(final long maxMsToWaitUntilConnected) + throws ZkInterruptedException, ZkTimeoutException, IllegalStateException { + connect(maxMsToWaitUntilConnected, this); + } + /** * Connect to ZooKeeper. * @param maxMsToWaitUntilConnected @@ -2507,12 +2638,18 @@ public Object call() throws Exception { */ public void connect(final long maxMsToWaitUntilConnected, Watcher watcher) throws ZkInterruptedException, ZkTimeoutException, IllegalStateException { - if (isClosed()) { - throw new IllegalStateException("ZkClient already closed!"); - } boolean started = false; - acquireEventLock(); + try { + acquireEventLock(); + + if (isClosed()) { + throw new IllegalStateException("ZkClient already closed!"); + } + if (_currentState != null) { + throw new IllegalStateException( + "ZkClient is not in init state. connect() has already been called."); + } setShutdownTrigger(false); IZkConnection zkConnection = getConnection(); @@ -2533,8 +2670,7 @@ public void connect(final long maxMsToWaitUntilConnected, Watcher watcher) zkConnection.connect(watcher); LOG.debug("zkclient{} Awaiting connection to Zookeeper server", _uid); if (!waitUntilConnected(maxMsToWaitUntilConnected, TimeUnit.MILLISECONDS)) { - throw new ZkTimeoutException( - "Unable to connect to zookeeper server within timeout: " + maxMsToWaitUntilConnected); + throw new ZkTimeoutException("Unable to connect to zookeeper server within timeout: " + maxMsToWaitUntilConnected); } } else { // if the client is not managing connection, the input connection is supposed to connect. @@ -2559,6 +2695,13 @@ public void connect(final long maxMsToWaitUntilConnected, Watcher watcher) close(); } } + try { + if (_monitor != null) { + _monitor.register(); + } + } catch (JMException e){ + LOG.error("Error in creating ZkClientMonitor", e); + } } public long getCreationTime(String path) { @@ -2600,6 +2743,7 @@ public void close() throws ZkInterruptedException { } _eventThread.interrupt(); _eventThread.join(2000); + // TODO: Closing _event thread here will miss final `CLOSE` state change. if (isManagingZkConnection()) { LOG.info("Closing zkclient uid:{}, zk:{}", _uid, ((ZkConnection) connection).getZookeeper()); connection.close(); @@ -2891,4 +3035,122 @@ private void validateWriteSizeLimitConfig() { + " is greater than ZkClient size limit " + WRITE_SIZE_LIMIT); } } + + private void addDataListener(String path, IZkDataListener listener) { + Set listenerEntries; + listenerEntries = _dataListener.get(path); + if (listenerEntries == null) { + listenerEntries = new CopyOnWriteArraySet<>(); + _dataListener.put(path, listenerEntries); + } + + boolean prefetchEnabled = isPrefetchEnabled(listener); + IZkDataListenerEntry listenerEntry = new IZkDataListenerEntry(listener, prefetchEnabled); + listenerEntries.add(listenerEntry); + if (prefetchEnabled) { + LOG.debug("zkclient {} subscribed data changes for {}, listener {}, prefetch data {}", _uid, + path, listener, prefetchEnabled); + } + } + + private void removeDataListener(String path, IZkDataListener dataListener) { + final Set listeners = _dataListener.get(path); + if (listeners != null) { + IZkDataListenerEntry listenerEntry = new IZkDataListenerEntry(dataListener); + listeners.remove(listenerEntry); + } + if (listeners == null || listeners.isEmpty()) { + _dataListener.remove(path); + } + } + + private void addChildListener(String path, IZkChildListener listener) { + Set listeners = _childListener.get(path); + if (listeners == null) { + listeners = new CopyOnWriteArraySet<>(); + _childListener.put(path, listeners); + } + listeners.add(listener); + } + + private void removeChildListener(String path, IZkChildListener listener) { + final Set listeners = _childListener.get(path); + if (listeners != null) { + listeners.remove(listener); + } + } + + interface ManipulateListener { + void run() throws KeeperException, InterruptedException; + } + + // Add a persist listener on the path. + // Throws UnsupportedOperationException if there is already a recursive persist listener on the + // path because it will overwrite that recursive persist listener. + private void addPersistListener(String path, Object listener) { + ManipulateListener addListeners = () -> { + if (_zkPathRecursiveWatcherTrie.hasListenerOnPath(path)) { + throw new UnsupportedOperationException( + "Can not subscribe PersistListener when there is an recursive listener on path: " + + path); + } + if (listener instanceof IZkChildListener) { + addChildListener(path, (IZkChildListener) listener); + } else if (listener instanceof IZkDataListener) { + addDataListener(path, (IZkDataListener) listener); + } + }; + executeWithInPersistListenerMutex(addListeners); + } + + + // TODO: Consider create an empty interface and let the two listeners interface extend that + // interface for code clean. + // This function removes persist child or data listener. + private void removePersistListener(String path, Object listener) { + + ManipulateListener removeListeners = () -> { + try { + if (listener instanceof IZkChildListener) { + removeChildListener(path, (IZkChildListener) listener); + } else if (listener instanceof IZkDataListener) { + removeDataListener(path, (IZkDataListener) listener); + } + if (!hasChildOrDataListeners(path)) { + // This will also remove persist recursive watcher on ZK. However, there should not be an + // persist recursive watcher installed in the first place. + getConnection().removeWatches(path, this, WatcherType.Any); + } + } catch (KeeperException.NoWatcherException e) { + LOG.warn("Persist watcher is already removed"); + } + }; + + executeWithInPersistListenerMutex(removeListeners); + } + + private void executeWithInPersistListenerMutex(ManipulateListener runnable) { + try { + _persistListenerMutex.lockInterruptibly(); + runnable.run(); + } catch (KeeperException.NoWatcherException e) { + LOG.warn("Persist watcher is already removed"); + } catch (KeeperException | InterruptedException ex) { + throw new ZkException(ex); + } finally { + _persistListenerMutex.unlock(); + } + } + + /* + Throws exception when try to subscribe watch when using _usePersistWatcher. When ZkClient + is subscribed as persist watcher, resubscribing the same object as onw time watcher will + over write the persist watcher causing missing following event. + */ + private void validateNativeZkWatcherType(boolean watch) { + if (_usePersistWatcher && watch) { + throw new IllegalArgumentException( + "Can not subscribe one time watcher when ZkClient is using PersistWatcher"); + } + } } diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkConnection.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkConnection.java index 01935919cb..376409231c 100644 --- a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkConnection.java +++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkConnection.java @@ -29,6 +29,7 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.helix.zookeeper.constant.ZkSystemPropertyKeys; import org.apache.helix.zookeeper.zkclient.exception.ZkException; +import org.apache.zookeeper.AddWatchMode; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.Op; @@ -244,6 +245,18 @@ public void addAuthInfo(String scheme, byte[] auth) { _zk.addAuthInfo(scheme, auth); } + @Override + public void addWatch(String basePath, Watcher watcher, AddWatchMode mode) + throws KeeperException, InterruptedException { + _zk.addWatch(basePath, watcher, mode); + } + + @Override + public void removeWatches(String path, Watcher watcher, Watcher.WatcherType watcherType) + throws InterruptedException, KeeperException { + _zk.removeWatches(path, watcher, watcherType, true); + } + private void lookupGetChildrenMethod() { _getChildrenMethod = doLookUpGetChildrenMethod(); diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/callback/ZkAsyncCallbacks.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/callback/ZkAsyncCallbacks.java index ab38b4ed60..61c97c2313 100644 --- a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/callback/ZkAsyncCallbacks.java +++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/callback/ZkAsyncCallbacks.java @@ -54,6 +54,14 @@ public void processResult(int rc, String path, Object ctx, byte[] data, Stat sta callback(rc, path, ctx); } + public Stat getStat() { + return _stat; + } + + public byte[] getData() { + return _data; + } + @Override public void handle() { // TODO Auto-generated method stub @@ -61,7 +69,7 @@ public void handle() { @Override protected void recordFailure(int rc, String path, ZkAsyncCallMonitorContext monitor) { - if(rc != Code.NONODE.intValue()) { + if (rc != Code.NONODE.intValue()) { monitor.recordFailure(path); } } @@ -99,6 +107,10 @@ public void processResult(int rc, String path, Object ctx, Stat stat) { callback(rc, path, ctx); } + public Stat getStat() { + return _stat; + } + @Override public void handle() { // TODO Auto-generated method stub @@ -106,7 +118,7 @@ public void handle() { @Override protected void recordFailure(int rc, String path, ZkAsyncCallMonitorContext monitor) { - if(rc != Code.NONODE.intValue()) { + if (rc != Code.NONODE.intValue()) { monitor.recordFailure(path); } } @@ -182,6 +194,7 @@ protected boolean needRetry(int rc) { public static abstract class DefaultCallback implements CancellableZkAsyncCallback { AtomicBoolean _isOperationDone = new AtomicBoolean(false); int _rc = KeeperException.Code.APIERROR.intValue(); + String _path; public void callback(int rc, String path, Object ctx) { if (rc != 0) { @@ -198,12 +211,14 @@ public void callback(int rc, String path, Object ctx) { } _rc = rc; + _path = path; // If retry is requested by passing the retry callback context, do retry if necessary. if (needRetry(rc)) { if (ctx != null && ctx instanceof ZkAsyncRetryCallContext) { try { if (((ZkAsyncRetryCallContext) ctx).requestRetry()) { + LOG.info("Received {} for async request on path {}, requested retry.", rc, path); // The retry operation will be done asynchronously. Once it is done, the same callback // handler object shall be triggered to ensure the result is notified to the right // caller(s). @@ -225,6 +240,8 @@ public void callback(int rc, String path, Object ctx) { // If operation is done successfully or no retry needed, notify the caller(s). try { handle(); + } catch (Exception ex) { + LOG.error("Exception while handling user callback for path {}.", _path, ex); } finally { markOperationDone(); } @@ -259,9 +276,14 @@ public int getRc() { return _rc; } + public String getPath() { + return _path; + } + @Override public void notifyCallers() { LOG.warn("The callback {} has been cancelled.", this); + handle(); markOperationDone(); } diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/metric/ZkClientMonitor.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/metric/ZkClientMonitor.java index 13231c8421..c3b338690e 100644 --- a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/metric/ZkClientMonitor.java +++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/metric/ZkClientMonitor.java @@ -55,6 +55,7 @@ public enum AccessType { private String _monitorKey; private String _monitorInstanceName; private boolean _monitorRootOnly; + private volatile boolean _registered = false; private SimpleDynamicMetric _stateChangeEventCounter; private SimpleDynamicMetric _expiredSessionCounter; @@ -123,6 +124,9 @@ public synchronized boolean setAndInitZkEventThreadMonitor(ZkEventThread zkEvent @Override public DynamicMBeanProvider register() throws JMException { + if (_registered) { + return this; + } List> attributeList = new ArrayList<>(); attributeList.add(_dataChangeEventCounter); attributeList.add(_outstandingRequestGauge); @@ -143,6 +147,7 @@ public DynamicMBeanProvider register() throws JMException { } } }); + _registered = true; return this; } @@ -154,6 +159,7 @@ public void unregister() { for (ZkClientPathMonitor zkClientPathMonitor : _zkClientPathMonitorMap.values()) { zkClientPathMonitor.unregister(); } + _registered = false; } @Override diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/util/ZkPathRecursiveWatcherTrie.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/util/ZkPathRecursiveWatcherTrie.java new file mode 100644 index 0000000000..41a9dd1778 --- /dev/null +++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/util/ZkPathRecursiveWatcherTrie.java @@ -0,0 +1,264 @@ +package org.apache.helix.zookeeper.zkclient.util; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.helix.zookeeper.zkclient.RecursivePersistListener; + +/** + * ZkPathRecursiveWatcherTrie will be used as a registry of persistent recursive watchers. + * When persist recursive watcher is registered on path /X, ZK will send out data change for any + * data/child changing under the tree structure of /X. The event only include the path of changed + * ZNode. + * In ZkClient, when ever we get a dataChange event for path /X/Y/Z, we need to track back the path + * and notify all registered recursive persist listener and notify them about the node change. + * ref: https://zookeeper.apache.org/doc/r3.7.1/zookeeperProgrammers.html#sc_WatchPersistentRecursive + */ +public class ZkPathRecursiveWatcherTrie { + + /** Root node of PathTrie */ + private final TrieNode _rootNode; + + static class TrieNode { + + final String _value; // Segmented ZNode path at current level + // A map of segmented ZNode path of next level to TrieNode, We keep the same initial + // children size as Zk server + final Map _children = new HashMap<>(4); + // A list of recursive persist watcher on the current path + Set _recursiveListeners = new HashSet<>(4); + + /** + * Create a trie node with parent as parameter. + * + * @param value the value stored in this node + */ + private TrieNode(String value) { + _value = value; + } + + /** + * The value stored in this node. + * + * @return the value stored in this node + */ + public String getValue() { + return this._value; + } + + /** + * Add a child to the existing node. + * + * @param childName the string name of the child + * @param node the node that is the child + */ + void addChild(String childName, TrieNode node) { + this._children.putIfAbsent(childName, node); + } + + /** + * Return the child of a node mapping to the input child name. + * + * @param childName the name of the child + * @return the child of a node + */ + @VisibleForTesting + TrieNode getChild(String childName) { + return this._children.get(childName); + } + + /** + * Get the list of children of this trienode. + * + * @return A collection containing the node's children + */ + @VisibleForTesting + Map getChildren() { + return _children; + } + + /** + * Get the set of RecursivePersistWatcherListener + * Returns an empty set if no listener is registered on the path + * @return + */ + @VisibleForTesting + Set getRecursiveListeners() { + return _recursiveListeners; + } + + @Override + public String toString() { + return "TrieNode [name=" + _value + ", children=" + _children.keySet() + "]"; + } + } + + /** + * Construct a new PathTrie with a root node. + */ + public ZkPathRecursiveWatcherTrie() { + this._rootNode = new TrieNode( "/"); + } + + /** + * Add a path to the path trie. All paths are relative to the root node. + * + * @param path the path to add RecursivePersistListener + * @param listener the RecursivePersistListener to be added + */ + public void addRecursiveListener(final String path, RecursivePersistListener listener) { + Objects.requireNonNull(path, "Path cannot be null"); + + if (path.isEmpty()) { + throw new IllegalArgumentException("Empty path: " + path); + } + final List pathComponents = split(path); + + synchronized (this) { + TrieNode parent = _rootNode; + for (final String part : pathComponents) { + parent = parent.getChildren().computeIfAbsent(part, TrieNode::new); + } + parent._recursiveListeners.add(listener); + } + } + + public Set getAllRecursiveListeners(String path) { + Objects.requireNonNull(path, "Path cannot be null"); + + final List pathComponents = split(path); + Set result = new HashSet<>(); + synchronized (this) { + TrieNode cur = _rootNode; + for (final String element : pathComponents) { + cur = cur.getChild(element); + if (cur == null) { + break; + } + result.addAll(cur.getRecursiveListeners()); + } + } + return result; + } + + /** + * Removing a RecursivePersistWatcherListener on a path. + * + * Delete a path from the nearest trie node to current node if this is the only listener and there + * is no child on the trie node. + * + * @param path the of the lister registered + * @param listener the RecursivePersistListener to be removed + */ + public void removeRecursiveListener(final String path, RecursivePersistListener listener) { + Objects.requireNonNull(path, "Path cannot be null"); + + if (path.length() == 0) { + throw new IllegalArgumentException("Invalid path: " + path); + } + final List pathComponents = split(path); + + synchronized (this) { + TrieNode cur = _rootNode; + TrieNode highestNodeForDelete = + null; // track the highest node that from that node to leaf node. + TrieNode prevDeletable = _rootNode; + for (final String part : pathComponents) { + cur = cur.getChild(part); + if (cur == null) { + return; + } + // Every time when we move down one level of trie node, 3 pointers may be updated. + // highestNodeForDelete, prev of highestNodeForDelete and cur TrieNode. + // we invalidate `highestNodeForDelete` when cur node is non leaf node but has more than one + // children or listener, or it is leaf node and has more than one listener. + + boolean candidateToDelete = + (cur.getChildren().size() == 1 && cur.getRecursiveListeners().isEmpty()) || ( + cur.getChildren().isEmpty() && cur.getRecursiveListeners().size() == 1 && cur + .getRecursiveListeners().contains(listener)); + if (candidateToDelete) { + if (highestNodeForDelete == null) { + highestNodeForDelete = cur; + } + } else { + prevDeletable = cur; + highestNodeForDelete = null; + } + } + if (!cur.getRecursiveListeners().contains(listener)) { + return; + } + cur.getRecursiveListeners().remove(listener); + + if (highestNodeForDelete != null) { + prevDeletable.getChildren().remove(highestNodeForDelete.getValue()); + } + } + } + + /** + * Return if there is listener on a particular path + * @param path + * @return + */ + public boolean hasListenerOnPath(String path) { + Objects.requireNonNull(path, "Path cannot be null"); + + final List pathComponents = split(path); + TrieNode cur; + synchronized (this) { + cur = _rootNode; + for (final String element : pathComponents) { + cur = cur.getChild(element); + if (cur == null) { + break; + } + } + } + return cur != null && !cur.getRecursiveListeners().isEmpty(); + } + + /** + * Clear all nodes in the trie. + */ + public synchronized void clear() { + _rootNode.getChildren().clear(); + } + + private static List split(final String path) { + return Stream.of(path.split("/")).filter(t -> !t.trim().isEmpty()).collect(Collectors.toList()); + } + + // only for test + @VisibleForTesting + TrieNode getRootNode() { + return _rootNode; + } +} diff --git a/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/TestZooKeeperConnection.java b/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/TestZooKeeperConnection.java new file mode 100644 index 0000000000..0ea8524dbb --- /dev/null +++ b/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/TestZooKeeperConnection.java @@ -0,0 +1,115 @@ +package org.apache.helix.zookeeper.impl; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.helix.zookeeper.zkclient.IZkConnection; +import org.apache.helix.zookeeper.zkclient.ZkClient; +import org.apache.zookeeper.AddWatchMode; +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.WatchedEvent; +import org.apache.zookeeper.Watcher; +import org.apache.zookeeper.ZooDefs; +import org.testng.Assert; +import org.testng.annotations.Test; + + +public class TestZooKeeperConnection extends ZkTestBase { + final int count = 100; + final AtomicInteger[] get_count = {new AtomicInteger(0)}; + CountDownLatch countDownLatch = new CountDownLatch(count*2); + CountDownLatch countDownLatch2 = new CountDownLatch(count*3); + + + /* + This function tests persist watchers' behavior in {@link org.apache.helix.zookeeper.zkclient.ZkConnection} + 1. Register a persist watcher on a path and create 100 children Znode, edit the ZNode for 100 times. + Expecting 200 events. + 2. register a one time listener on the path. Make the same change and count the total number of event. + */ + + @Test + void testPersistWatcher() throws Exception { + Watcher watcher1 = new PersistWatcher(); + ZkClient zkClient = new org.apache.helix.zookeeper.impl.client.ZkClient(ZK_ADDR); + IZkConnection _zk = zkClient.getConnection(); + String path="/testPersistWatcher"; + _zk.create(path, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); + // register a persist listener on a path, change the ZNode 100 times, create 100 child ZNode, + // and expecting 200 events + _zk.addWatch(path, watcher1, AddWatchMode.PERSISTENT); + for (int i=0; i { + return (get_count[0].get() >= 202 & get_count[0].get() < 400); + }, TestHelper.WAIT_DURATION)); + zkClient.close(); + } + + @Test (dependsOnMethods = "testPersistWatcher") + void testRecursivePersistWatcherWithOneTimeWatcher() throws Exception { + // reset counter + get_count[0].set(0); + Watcher watcher1 = new PersistRecurWatcher(); + ZkClient zkClient = new org.apache.helix.zookeeper.impl.client.ZkClient(ZK_ADDR); + IZkConnection _zk = zkClient.getConnection(); + String path="/testRecursivePersistWatcher"; + _zk.create(path, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); + // register a persist listener on a path, change the ZNode 100 times, create 100 child ZNode, + // and expecting 200 events + _zk.addWatch(path, watcher1, AddWatchMode.PERSISTENT_RECURSIVE); + for (int i=0; i { + return (get_count[0].get() >= 302 && get_count[0].get() < 500); + }, TestHelper.WAIT_DURATION)); + zkClient.close(); + } + + class PersistWatcher implements Watcher { + @Override + public void process(WatchedEvent watchedEvent) { + get_count[0].incrementAndGet(); + countDownLatch.countDown(); + } + } + + class PersistRecurWatcher implements Watcher { + @Override + public void process(WatchedEvent watchedEvent) { + get_count[0].incrementAndGet(); + countDownLatch2.countDown(); + } + } + +} \ No newline at end of file diff --git a/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/TestZkClientAsyncRetry.java b/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/TestZkClientAsyncRetry.java index e55f8e6a95..ccbbde0d7b 100644 --- a/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/TestZkClientAsyncRetry.java +++ b/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/TestZkClientAsyncRetry.java @@ -238,11 +238,89 @@ public void testAsyncWriteRetry() throws JMException { } } + @Test(dependsOnMethods = "testAsyncWriteRetry") + public void testAsyncRetryCustomizedCallback() throws JMException { + // int array to store customized async callback return value. Initial value set to 100, witch + // not used by any ZK return code. + final int[] _returnCode = new int[2]; + _returnCode[0] = 100; + _returnCode[1] = 100; + + // Define Customized callback + class CustomizedSetCallback extends ZkAsyncCallbacks.SetDataCallbackHandler { + @Override + public void handle() { + _returnCode[0] = getRc(); + } + } + + MockAsyncZkClient testZkClient = new MockAsyncZkClient(_zkServerAddress); + try { + ZNRecord tmpRecord = new ZNRecord("tmpRecord"); + tmpRecord.setSimpleField("foo", "bar"); + testZkClient.createPersistent(NODE_PATH, tmpRecord); + + // 1. Test async set retry + CustomizedSetCallback setCallback = + new CustomizedSetCallback(); + Assert.assertEquals(setCallback.getRc(), KeeperException.Code.APIERROR.intValue()); + + tmpRecord.setSimpleField("test", "data"); + testZkClient.setAsyncCallRC(CONNECTIONLOSS.intValue()); + // Async set will be pending due to the mock error rc is retryable. + testZkClient.asyncSetData(NODE_PATH, tmpRecord, -1, setCallback); + Assert.assertFalse(setCallback.isOperationDone()); + Assert.assertEquals(setCallback.getRc(), CONNECTIONLOSS.intValue()); + // handle() haven't been called until retry finished or canceled, assert it is default value. + Assert.assertEquals(_returnCode[0], 100); + // Change the mock return code. + testZkClient.setAsyncCallRC(KeeperException.Code.OK.intValue()); + // Async retry will succeed now. Wait until the operation is successfully done and verify. + Assert.assertTrue(waitAsyncOperation(setCallback, RETRY_OPS_WAIT_TIMEOUT_MS)); + Assert.assertEquals(setCallback.getRc(), KeeperException.Code.OK.intValue()); + // handle() called when retry finished, check return value. + Assert.assertEquals(_returnCode[0], KeeperException.Code.OK.intValue()); + Assert.assertTrue(testZkClient.getAndResetRetryCount() >= 1); + + // 2. Test async delete + class CustomizedDeleteCallback extends ZkAsyncCallbacks.DeleteCallbackHandler{ + @Override + public void handle() { + _returnCode[1] = getRc(); + } + } + CustomizedDeleteCallback deleteCallback = + new CustomizedDeleteCallback(); + Assert.assertEquals(deleteCallback.getRc(), KeeperException.Code.APIERROR.intValue()); + + testZkClient.setAsyncCallRC(CONNECTIONLOSS.intValue()); + // Async delete will be pending due to the mock error rc is retryable. + testZkClient.asyncDelete(NODE_PATH, deleteCallback); + Assert.assertFalse(deleteCallback.isOperationDone()); + Assert.assertEquals(deleteCallback.getRc(), CONNECTIONLOSS.intValue()); + // handle() haven't been called until retry finished or canceled, assert it is default value. + Assert.assertEquals(_returnCode[1], 100); + // Change the mock return code. + testZkClient.setAsyncCallRC(KeeperException.Code.OK.intValue()); + // Async retry will succeed now. Wait until the operation is successfully done and verify. + Assert.assertTrue(waitAsyncOperation(deleteCallback, RETRY_OPS_WAIT_TIMEOUT_MS)); + Assert.assertEquals(deleteCallback.getRc(), KeeperException.Code.OK.intValue()); + Assert.assertFalse(testZkClient.exists(NODE_PATH)); + Assert.assertTrue(testZkClient.getAndResetRetryCount() >= 1); + // handle() called when retry finished, check return value. + Assert.assertEquals(_returnCode[1], KeeperException.Code.OK.intValue()); + } finally { + testZkClient.setAsyncCallRC(KeeperException.Code.OK.intValue()); + testZkClient.close(); + _zkClient.delete(NODE_PATH); + } + } + /* * Tests if exception is thrown during retry operation, * the context should be cancelled correctly. */ - @Test(dependsOnMethods = "testAsyncWriteRetry") + @Test(dependsOnMethods = "testAsyncRetryCustomizedCallback") public void testAsyncWriteRetryThrowException() throws JMException { MockAsyncZkClient testZkClient = new MockAsyncZkClient(_zkServerAddress); try { @@ -305,7 +383,95 @@ public void testAsyncWriteRetryThrowException() throws JMException { } } + /* + * Test handle() is executed once if callback retry is canceled. + */ @Test(dependsOnMethods = "testAsyncWriteRetryThrowException") + public void testAsyncRetryCustomizedCallbackCancel() throws JMException { + // int array to store customized async callback return value. Initial value set to 100, witch + // not used by any ZK return code. + final int[] _returnCode = new int[2]; + _returnCode[0] = 100; + _returnCode[1] = 100; + + // Define Customized callback + class CustomizedCreateCallback extends ZkAsyncCallbacks.CreateCallbackHandler { + @Override + public void handle() { + _returnCode[0] = getRc(); + } + } + + MockAsyncZkClient testZkClient = new MockAsyncZkClient(_zkServerAddress); + try { + ZNRecord tmpRecord = new ZNRecord("tmpRecord"); + tmpRecord.setSimpleField("foo", "bar"); + testZkClient.createPersistent(NODE_PATH, tmpRecord); + + // 1. Test async create retry + CustomizedCreateCallback createCallback = + new CustomizedCreateCallback(); + Assert.assertEquals(createCallback.getRc(), KeeperException.Code.APIERROR.intValue()); + + tmpRecord.setSimpleField("test", "data"); + testZkClient.setAsyncCallRC(CONNECTIONLOSS.intValue()); + // Async set will be pending due to the mock error rc is retryable. + testZkClient.asyncCreate(NODE_PATH, tmpRecord, CreateMode.PERSISTENT, createCallback); + Assert.assertFalse(createCallback.isOperationDone()); + // Original callback should have return code set to CONNECTIONLOSS + Assert.assertEquals(createCallback.getRc(), CONNECTIONLOSS.intValue()); + // handle() haven't been called until retry finished or canceled, assert it is default value. + Assert.assertEquals(_returnCode[0], 100); + // Throw exception in retry + testZkClient.setZkExceptionInRetry(true); + // Async retry will succeed now. Wait until the operation is done and verify. + Assert.assertTrue(waitAsyncOperation(createCallback, RETRY_OPS_WAIT_TIMEOUT_MS), + "Async callback should have been canceled"); + Assert.assertEquals(createCallback.getRc(), CONNECTIONLOSS.intValue()); + Assert.assertEquals(_returnCode[0], CONNECTIONLOSS.intValue()); + Assert.assertTrue(testZkClient.getAndResetRetryCount() >= 1); + + // Restore the state + testZkClient.setZkExceptionInRetry(false); + + class CustomizedSetCallback extends ZkAsyncCallbacks.SetDataCallbackHandler { + @Override + public void handle() { + _returnCode[1] = getRc(); + } + } + + // 1. Test async set retry + CustomizedSetCallback setCallback = + new CustomizedSetCallback(); + Assert.assertEquals(setCallback.getRc(), KeeperException.Code.APIERROR.intValue()); + + tmpRecord.setSimpleField("test", "data"); + testZkClient.setAsyncCallRC(CONNECTIONLOSS.intValue()); + // Async set will be pending due to the mock error rc is retryable. + testZkClient.asyncSetData(NODE_PATH, tmpRecord, -1, setCallback); + Assert.assertFalse(setCallback.isOperationDone()); + // Original callback should have return code set to CONNECTIONLOSS + Assert.assertEquals(createCallback.getRc(), CONNECTIONLOSS.intValue()); + // handle() haven't been called until retry finished or canceled, assert it is default value. + Assert.assertEquals(_returnCode[1], 100); + // Throw exception in retry + testZkClient.setZkExceptionInRetry(true); + // Async retry will succeed now. Wait until the operation is done and verify. + Assert.assertTrue(waitAsyncOperation(setCallback, RETRY_OPS_WAIT_TIMEOUT_MS), + "Async callback should have been canceled"); + Assert.assertEquals(setCallback.getRc(), CONNECTIONLOSS.intValue()); + Assert.assertEquals(_returnCode[1], CONNECTIONLOSS.intValue()); + Assert.assertTrue(testZkClient.getAndResetRetryCount() >= 1); + + } finally { + testZkClient.setAsyncCallRC(KeeperException.Code.OK.intValue()); + testZkClient.close(); + _zkClient.delete(NODE_PATH); + } + } + + @Test(dependsOnMethods = "testAsyncRetryCustomizedCallbackCancel") public void testAsyncReadRetry() throws JMException { MockAsyncZkClient testZkClient = new MockAsyncZkClient(_zkServerAddress); try { diff --git a/zookeeper-api/src/test/java/org/apache/helix/zookeeper/zkclient/TestZkClientPersistWatcher.java b/zookeeper-api/src/test/java/org/apache/helix/zookeeper/zkclient/TestZkClientPersistWatcher.java new file mode 100644 index 0000000000..76f5352c98 --- /dev/null +++ b/zookeeper-api/src/test/java/org/apache/helix/zookeeper/zkclient/TestZkClientPersistWatcher.java @@ -0,0 +1,210 @@ +package org.apache.helix.zookeeper.zkclient; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.helix.zookeeper.impl.ZkTestBase; +import org.apache.helix.zookeeper.impl.client.ZkClient; +import org.apache.helix.zookeeper.zkclient.serialize.BasicZkSerializer; +import org.apache.helix.zookeeper.zkclient.serialize.SerializableSerializer; +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.Watcher; +import org.testng.Assert; +import org.testng.annotations.Test; + + +public class TestZkClientPersistWatcher extends ZkTestBase { + + @Test + void testZkClientDataChange() throws Exception { + org.apache.helix.zookeeper.impl.client.ZkClient.Builder builder = + new org.apache.helix.zookeeper.impl.client.ZkClient.Builder(); + builder.setZkServer(ZkTestBase.ZK_ADDR).setMonitorRootPathOnly(false) + .setUsePersistWatcher(true); + org.apache.helix.zookeeper.impl.client.ZkClient zkClient = builder.build(); + zkClient.setZkSerializer(new BasicZkSerializer(new SerializableSerializer())); + int count = 1000; + final int[] event_count = {0}; + CountDownLatch countDownLatch1 = new CountDownLatch(count); + String path = "/dataChangeTestPath"; + IZkDataListener dataListener = new IZkDataListener() { + @Override + public void handleDataChange(String dataPath, Object data) throws Exception { + countDownLatch1.countDown(); + event_count[0]++; + } + + @Override + public void handleDataDeleted(String dataPath) throws Exception { + } + }; + + zkClient.subscribeDataChanges(path, dataListener); + zkClient.create(path, "datat", CreateMode.PERSISTENT); + for(int i=0; i currentChilds) + throws Exception { + countDownLatch1.countDown(); + event_count[0]++ ; + } + }; + IZkChildListener childListener2 = new IZkChildListener() { + @Override + public void handleChildChange(String parentPath, List currentChilds) + throws Exception { + countDownLatch2.countDown(); + event_count[0]++; + } + }; + zkClient.subscribeChildChanges(path, childListener); + zkClient.subscribeChildChanges(path, childListener2); + zkClient.create(path, "datat", CreateMode.PERSISTENT); + for (int i = 0; i < count; ++i) { + zkClient.create(path + "/child" + i, "datat", CreateMode.PERSISTENT); + } + Assert.assertTrue(countDownLatch1.await(15000, TimeUnit.MILLISECONDS)); + Assert.assertTrue(countDownLatch2.await(15000, TimeUnit.MILLISECONDS)); + zkClient.close(); + } + + @Test(dependsOnMethods = "testZkClientChildChange") + void testZkClientPersistRecursiveChange() throws Exception { + org.apache.helix.zookeeper.impl.client.ZkClient.Builder builder = + new org.apache.helix.zookeeper.impl.client.ZkClient.Builder(); + builder.setZkServer(ZkTestBase.ZK_ADDR).setMonitorRootPathOnly(false) + .setUsePersistWatcher(true); + org.apache.helix.zookeeper.impl.client.ZkClient zkClient = builder.build(); + zkClient.setZkSerializer(new BasicZkSerializer(new SerializableSerializer())); + int count = 100; + final AtomicInteger[] event_count = {new AtomicInteger(0)}; + final AtomicInteger[] event_count2 = {new AtomicInteger(0)}; + // for each iteration, we will edit a node, create a child, create a grand child, and + // delete child. Expect 4 event per iteration. -> total event should be count*4 + CountDownLatch countDownLatch1 = new CountDownLatch(count*4); + CountDownLatch countDownLatch2 = new CountDownLatch(count); + String path = "/testZkClientPersistRecursiveChange"; + RecursivePersistListener rcListener = new RecursivePersistListener() { + @Override + public void handleZNodeChange(String dataPath, Watcher.Event.EventType eventType) + throws Exception { + countDownLatch1.countDown(); + event_count[0].incrementAndGet() ; + } + }; + zkClient.create(path, "datat", CreateMode.PERSISTENT); + zkClient.subscribePersistRecursiveListener(path, rcListener); + for (int i=0; i currentChilds) + throws Exception { + countDownLatch2.countDown(); + event_count2[0].incrementAndGet(); + } + }; + try { + zkClient.subscribeChildChanges(path, childListener2, false); + } catch ( Exception ex) { + Assert.assertEquals(ex.getClass().getName(), "java.lang.UnsupportedOperationException"); + } + + // unsubscribe recursive persist watcher, and subscribe persist watcher should success. + zkClient.unsubscribePersistRecursiveListener(path, rcListener); + zkClient.subscribeChildChanges(path, childListener2, false); + // we should only get 100 event since only 100 direct child change. + for (int i=0; i