From 790666c557862379b7f762bedc281071238e3630 Mon Sep 17 00:00:00 2001 From: Kevin Herron Date: Thu, 15 Aug 2024 15:59:11 -0700 Subject: [PATCH] Prepare for 1.0 release (#6) Java 11, no slf4j, user-configurable context, Maven build, GH actions --- .github/workflows/maven-deploy.yml | 26 + .github/workflows/maven-release.yml | 31 + .github/workflows/maven.yml | 26 + LICENSE | 202 --- LICENSE.md | 279 ++++ build.gradle.kts | 140 -- config/checkstyle/checkstyle.xml | 385 ++++++ gradle.properties | 3 - gradle/wrapper/gradle-wrapper.properties | 5 - gradlew | 185 --- gradlew.bat | 89 -- pom.xml | 307 +++++ settings.gradle | 2 - .../netty/fsm/ChannelActions.java | 103 +- .../digitalpetri/netty/fsm/ChannelFsm.java | 370 +++--- .../netty/fsm/ChannelFsmConfig.java | 167 ++- .../netty/fsm/ChannelFsmConfigBuilder.java | 424 +++--- .../netty/fsm/ChannelFsmFactory.java | 1137 +++++++++-------- .../netty/fsm/CompletionBuilders.java | 111 ++ .../com/digitalpetri/netty/fsm/Event.java | 173 +-- .../com/digitalpetri/netty/fsm/Scheduler.java | 74 +- .../com/digitalpetri/netty/fsm/State.java | 30 +- .../netty/fsm/util/CompletionBuilders.java | 106 -- .../netty/fsm/DisconnectingTransitions.kt | 33 - .../com/digitalpetri/netty/fsm/IdleActions.kt | 38 - .../netty/fsm/NotConnectedTransitions.kt | 39 - .../netty/fsm/ConnectedActionsTest.kt} | 20 +- .../netty/fsm/ConnectedTransitionsTest.kt} | 18 +- .../netty/fsm/ConnectingActionsTest.kt} | 20 +- .../netty/fsm/ConnectingTransitionsTest.kt} | 18 +- .../netty/fsm/DisconnectingActionsTest.kt} | 18 +- .../netty/fsm/DisconnectingTransitionsTest.kt | 27 + .../digitalpetri/netty/fsm/IdleActionsTest.kt | 31 + .../netty/fsm/IdleTransitionsTest.kt} | 18 +- .../netty/fsm/NotConnectedActionsTest.kt} | 19 +- .../netty/fsm/NotConnectedTransitionsTest.kt | 33 + .../netty/fsm/ReconnectWaitActionsTest.kt} | 18 +- .../fsm/ReconnectWaitTransitionsTest.kt} | 18 +- .../netty/fsm/ReconnectingActionsTest.kt} | 18 +- .../netty/fsm/ReconnectingTransitionsTest.kt} | 18 +- .../netty/fsm/TransitionListenersTest.kt} | 12 +- .../com/digitalpetri/netty/fsm/fsm.kt | 18 +- 42 files changed, 2561 insertions(+), 2248 deletions(-) create mode 100644 .github/workflows/maven-deploy.yml create mode 100644 .github/workflows/maven-release.yml create mode 100644 .github/workflows/maven.yml delete mode 100644 LICENSE create mode 100644 LICENSE.md delete mode 100644 build.gradle.kts create mode 100644 config/checkstyle/checkstyle.xml delete mode 100644 gradle.properties delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100755 gradlew delete mode 100644 gradlew.bat create mode 100644 pom.xml delete mode 100644 settings.gradle create mode 100644 src/main/java/com/digitalpetri/netty/fsm/CompletionBuilders.java delete mode 100644 src/main/java/com/digitalpetri/netty/fsm/util/CompletionBuilders.java delete mode 100644 src/test/java/com/digitalpetri/netty/fsm/DisconnectingTransitions.kt delete mode 100644 src/test/java/com/digitalpetri/netty/fsm/IdleActions.kt delete mode 100644 src/test/java/com/digitalpetri/netty/fsm/NotConnectedTransitions.kt rename src/test/{java/com/digitalpetri/netty/fsm/ConnectedActions.kt => kotlin/com/digitalpetri/netty/fsm/ConnectedActionsTest.kt} (85%) rename src/test/{java/com/digitalpetri/netty/fsm/ConnectedTransitions.kt => kotlin/com/digitalpetri/netty/fsm/ConnectedTransitionsTest.kt} (71%) rename src/test/{java/com/digitalpetri/netty/fsm/ConnectingActions.kt => kotlin/com/digitalpetri/netty/fsm/ConnectingActionsTest.kt} (86%) rename src/test/{java/com/digitalpetri/netty/fsm/ConnectingTransitions.kt => kotlin/com/digitalpetri/netty/fsm/ConnectingTransitionsTest.kt} (70%) rename src/test/{java/com/digitalpetri/netty/fsm/DisconnectingActions.kt => kotlin/com/digitalpetri/netty/fsm/DisconnectingActionsTest.kt} (88%) create mode 100644 src/test/kotlin/com/digitalpetri/netty/fsm/DisconnectingTransitionsTest.kt create mode 100644 src/test/kotlin/com/digitalpetri/netty/fsm/IdleActionsTest.kt rename src/test/{java/com/digitalpetri/netty/fsm/IdleTransitions.kt => kotlin/com/digitalpetri/netty/fsm/IdleTransitionsTest.kt} (62%) rename src/test/{java/com/digitalpetri/netty/fsm/NotConnectedActions.kt => kotlin/com/digitalpetri/netty/fsm/NotConnectedActionsTest.kt} (56%) create mode 100644 src/test/kotlin/com/digitalpetri/netty/fsm/NotConnectedTransitionsTest.kt rename src/test/{java/com/digitalpetri/netty/fsm/ReconnectWaitActions.kt => kotlin/com/digitalpetri/netty/fsm/ReconnectWaitActionsTest.kt} (93%) rename src/test/{java/com/digitalpetri/netty/fsm/ReconnectWaitTransitions.kt => kotlin/com/digitalpetri/netty/fsm/ReconnectWaitTransitionsTest.kt} (52%) rename src/test/{java/com/digitalpetri/netty/fsm/ReconnectingActions.kt => kotlin/com/digitalpetri/netty/fsm/ReconnectingActionsTest.kt} (87%) rename src/test/{java/com/digitalpetri/netty/fsm/ReconnectingTransitions.kt => kotlin/com/digitalpetri/netty/fsm/ReconnectingTransitionsTest.kt} (55%) rename src/test/{java/com/digitalpetri/netty/fsm/TransitionListeners.kt => kotlin/com/digitalpetri/netty/fsm/TransitionListenersTest.kt} (75%) rename src/test/{java => kotlin}/com/digitalpetri/netty/fsm/fsm.kt (91%) diff --git a/.github/workflows/maven-deploy.yml b/.github/workflows/maven-deploy.yml new file mode 100644 index 0000000..634cc9d --- /dev/null +++ b/.github/workflows/maven-deploy.yml @@ -0,0 +1,26 @@ +name: Maven Deploy + +on: + push: + branches: [ "master" ] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Maven Central Repository + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + + - name: Publish package + run: mvn --batch-mode deploy + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml new file mode 100644 index 0000000..fb0013d --- /dev/null +++ b/.github/workflows/maven-release.yml @@ -0,0 +1,31 @@ +name: Maven Release + +on: + release: + types: [ created ] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Maven Central Repository + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + + - name: Install GPG secret key + run: | + cat <(echo -e "${{ secrets.OSSRH_GPG_SECRET_KEY }}") | gpg --batch --import + gpg --list-secret-keys --keyid-format LONG + + - name: Publish package + run: mvn --batch-mode -Dgpg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} clean deploy -P release + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..5829512 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,26 @@ +name: Java CI with Maven + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml + + # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive + - name: Update dependency graph + uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index e06d208..0000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ -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. - diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..86875af --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,279 @@ +Eclipse Public License - v 2.0 +============================== + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE +PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION +OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +### 1. DEFINITIONS + +"Contribution" means: + +* a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + +* b) in the case of each subsequent Contributor: + * *i)* changes to the Program, and + * *ii)* additions to the Program; + + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +### 2. GRANT OF RIGHTS + +* a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + +* b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + +* c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + +* d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + +* e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +### 3. REQUIREMENTS + +* 3.1 If a Contributor Distributes the Program in any form, then: + + * a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + * b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + * *i)* effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + * *ii)* effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + * *iii)* does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + * *iv)* requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +* 3.2 When the Program is Distributed as Source Code: + + * a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + * b) a copy of this Agreement must be included with each copy of + the Program. + +* 3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +### 4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +### 5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED 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. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +### 6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), 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 OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +### 7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + + "This Source Code may also be made available under the following + Secondary Licenses when the conditions for such availability set forth + in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), + version(s), and exceptions or additional permissions here}." + +Simply including a copy of this Agreement, including this Exhibit A +is not sufficient to license the Source Code under Secondary Licenses. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to +look for such a notice. + +You may add additional accurate notices of copyright ownership. diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index 3efc829..0000000 --- a/build.gradle.kts +++ /dev/null @@ -1,140 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - `java-library` - kotlin("jvm") version "1.4.30" - `maven-publish` - signing -} - -group = "com.digitalpetri.netty" -version = "0.9-SNAPSHOT" - -repositories { - mavenCentral() -} - -dependencies { - api("com.digitalpetri.fsm:strict-machine:0.6") - - // BYO SLF4J - compileOnly("org.slf4j:slf4j-api:1.7.+") - - // BYO Netty - compileOnly("io.netty:netty-handler:4.0+") - - testImplementation(kotlin("stdlib", "1.4.30")) - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1") - testImplementation("io.netty:netty-handler:4.0+") - testImplementation("io.netty:netty-transport:4.0+") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.1") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.3.1") - testRuntimeOnly("org.slf4j:slf4j-simple:1.7.25") -} - -repositories { - mavenLocal() -} - -tasks { - withType { - sourceCompatibility = "1.8" - targetCompatibility = "1.8" - } - - withType { - kotlinOptions.jvmTarget = "1.8" - } - - withType { - useJUnitPlatform() - - testLogging { - events("PASSED", "FAILED", "SKIPPED") - } - } -} - -task("sourcesJar") { - from(sourceSets.main.get().allJava) - archiveClassifier.set("sources") -} - -task("javadocJar") { - from(tasks.javadoc) - archiveClassifier.set("javadoc") -} - -tasks.withType { - manifest { - attributes("Automatic-Module-Name" to "com.digitalpetri.netty.fsm") - } -} - -publishing { - publications { - create("mavenJava") { - artifactId = "netty-channel-fsm" - - from(components["java"]) - - artifact(tasks["sourcesJar"]) - artifact(tasks["javadocJar"]) - - pom { - name.set("Netty Channel FSM") - description.set("An FSM that manages async non-blocking access to a Netty Channel") - url.set("https://github.com/digitalpetri/netty-channel-fsm") - - licenses { - license { - name.set("The Apache License, Version 2.0") - url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") - } - } - - developers { - developer { - id.set("kevinherron") - name.set("Kevin Herron") - email.set("kevinherron@gmail.com") - } - } - - scm { - connection.set("scm:git:git://github.com.com/digitalpetri/netty-channel-fsm.git") - developerConnection.set("scm:git:ssh://github.com.com/digitalpetri/netty-channel-fsm.git") - url.set("https://github.com/digitalpetri/netty-channel-fsm") - } - } - } - } - - repositories { - maven { - credentials { - username = project.findProperty("ossrhUsername") as String? - password = project.findProperty("ossrhPassword") as String? - } - - val releasesRepoUrl = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") - val snapshotsRepoUrl = uri("https://oss.sonatype.org/content/repositories/snapshots/") - url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl - } - - mavenLocal() - } -} - -signing { - if (!version.toString().endsWith("SNAPSHOT")) { - useGpgCmd() - sign(publishing.publications["mavenJava"]) - } -} - -tasks.javadoc { - if (JavaVersion.current().isJava9Compatible) { - (options as StandardJavadocDocletOptions).addBooleanOption("html5", true) - } -} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7700b12 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,385 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 0992275..0000000 --- a/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -# Prevent the kotlin-jvm plugin from automatically adding a dependency on -# Kotlin stdlib. We only use Kotlin for tests, we don't need it. -kotlin.stdlib.default.dependency=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 442d913..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index 4f906e0..0000000 --- a/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# 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 -# -# https://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. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index 107acd3..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7ad7fb9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,307 @@ + + + 4.0.0 + + com.digitalpetri.netty + netty-channel-fsm + 1.0.0-SNAPSHOT + + Netty Channel FSM + + A state machine that manages async non-blocking access to a Netty Channel. + + https://github.com/digitalpetri/netty-channel-fsm + + + + kevinherron + Kevin Herron + kevinherron@gmail.com + + + + + + Eclipse Public License - v 2.0 + https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html + repo + + + + + https://github.com/digitalpetri/netty-channel-fsm + scm:git:git://github.com/digitalpetri/netty-channel-fsm.git + + scm:git:git@github.com:digitalpetri/netty-channel-fsm.git + + HEAD + + + + 11 + 11 + 11 + UTF-8 + + + 5.10.2 + 2.0.10 + + + 10.17.0 + 3.4.0 + 3.5.0 + + + + + com.digitalpetri.fsm + strict-machine + 1.0.0-RC1 + + + io.netty + netty-handler + 4.1.112.Final + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + test + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin.version} + test + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.8.0 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.4 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + v@{project.version} + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + true + + + -Xlint:all + + + + + default-compile + none + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + process-sources + + compile + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/kotlin + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-maven + + enforce + + + + + + 3.6.3 + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + config/checkstyle/checkstyle.xml + true + true + true + false + + + + validate + validate + + check + + + + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index e513e09..0000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = 'netty-channel-fsm' - diff --git a/src/main/java/com/digitalpetri/netty/fsm/ChannelActions.java b/src/main/java/com/digitalpetri/netty/fsm/ChannelActions.java index c1df5cc..4afca63 100644 --- a/src/main/java/com/digitalpetri/netty/fsm/ChannelActions.java +++ b/src/main/java/com/digitalpetri/netty/fsm/ChannelActions.java @@ -1,67 +1,62 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm; -import java.util.concurrent.CompletableFuture; - -import com.digitalpetri.strictmachine.FsmContext; +import com.digitalpetri.fsm.FsmContext; import io.netty.channel.Channel; +import java.util.concurrent.CompletableFuture; public interface ChannelActions { - /** - * Bootstrap a new {@link Channel} and return a {@link CompletableFuture} that completes successfully when the - * Channel is ready to use or completes exceptionally if the Channel could not be created or made ready to use for - * any reason. - * - * @param ctx the {@link FsmContext}. - * @return a {@link CompletableFuture} that completes successfully when the - * Channel is ready to use or completes exceptionally if the Channel could not be created or made ready to use for - * any reason. - */ - CompletableFuture connect(FsmContext ctx); - - /** - * Perform any disconnect actions and then close {@code channel}, returning a {@link CompletableFuture} that - * completes successfully when the Channel has disconnected or completes exceptionally if the channel could not be - * disconnected for any reason. - *

- * The state machine advances the same way regardless of how the future is completed. - * - * @param ctx the {@link FsmContext}. - * @param channel the {@link Channel} to disconnect. - * @return a {@link CompletableFuture} that completes successfully when the Channel - * * has disconnected or completes exceptionally if the channel could not be disconnected for any reason. - */ - CompletableFuture disconnect(FsmContext ctx, Channel channel); - - /** - * Perform a keep-alive action because the Channel has been idle for longer than {@code maxIdleSeconds}. - *

- * Although the keep-alive action is implementation dependent the intended usage would be to do something send a - * request that tests the Channel to make sure it's still valid. - * - * @param ctx the {@link FsmContext} - * @param channel the {@link Channel} to send the keep-alive on. - * @return a {@link CompletableFuture} that completes successfully if the channel is still valid and completes - * exceptionally otherwise. - */ - default CompletableFuture keepAlive(FsmContext ctx, Channel channel) { - return CompletableFuture.completedFuture(null); - } + /** + * Bootstrap a new {@link Channel} and return a {@link CompletableFuture} that completes + * successfully when the Channel is ready to use or completes exceptionally if the Channel could + * not be created or made ready to use for any reason. + * + * @param ctx the {@link FsmContext}. + * @return a {@link CompletableFuture} that completes successfully when the Channel is ready to + * use or completes exceptionally if the Channel could not be created or made ready to use for + * any reason. + */ + CompletableFuture connect(FsmContext ctx); + + /** + * Perform any disconnect actions and then close {@code channel}, returning a + * {@link CompletableFuture} that completes successfully when the Channel has disconnected or + * completes exceptionally if the channel could not be disconnected for any reason. + * + *

The state machine advances the same way regardless of how the future is completed. + * + * @param ctx the {@link FsmContext}. + * @param channel the {@link Channel} to disconnect. + * @return a {@link CompletableFuture} that completes successfully when the Channel * has + * disconnected or completes exceptionally if the channel could not be disconnected for any + * reason. + */ + CompletableFuture disconnect(FsmContext ctx, Channel channel); + + /** + * Perform a keep-alive action because the Channel has been idle for longer than + * {@code maxIdleSeconds}. + * + *

Although the keep-alive action is implementation dependent the intended usage would be to + * do something send a request that tests the Channel to make sure it's still valid. + * + * @param ctx the {@link FsmContext} + * @param channel the {@link Channel} to send the keep-alive on. + * @return a {@link CompletableFuture} that completes successfully if the channel is still valid + * and completes exceptionally otherwise. + */ + default CompletableFuture keepAlive(FsmContext ctx, Channel channel) { + return CompletableFuture.completedFuture(null); + } } diff --git a/src/main/java/com/digitalpetri/netty/fsm/ChannelFsm.java b/src/main/java/com/digitalpetri/netty/fsm/ChannelFsm.java index 38a835b..fe8f3f6 100644 --- a/src/main/java/com/digitalpetri/netty/fsm/ChannelFsm.java +++ b/src/main/java/com/digitalpetri/netty/fsm/ChannelFsm.java @@ -1,221 +1,223 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CopyOnWriteArrayList; - +import com.digitalpetri.fsm.Fsm; +import com.digitalpetri.fsm.FsmContext; +import com.digitalpetri.fsm.dsl.ActionContext; +import com.digitalpetri.fsm.dsl.FsmBuilder; +import com.digitalpetri.fsm.dsl.TransitionAction; import com.digitalpetri.netty.fsm.Event.Connect; import com.digitalpetri.netty.fsm.Event.Disconnect; import com.digitalpetri.netty.fsm.Event.GetChannel; import com.digitalpetri.netty.fsm.Scheduler.Cancellable; -import com.digitalpetri.strictmachine.Fsm; -import com.digitalpetri.strictmachine.FsmContext; -import com.digitalpetri.strictmachine.dsl.ActionContext; -import com.digitalpetri.strictmachine.dsl.FsmBuilder; -import com.digitalpetri.strictmachine.dsl.TransitionAction; import io.netty.channel.Channel; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; public class ChannelFsm { - private final List transitionListeners = new CopyOnWriteArrayList<>(); - - private final Fsm fsm; - - ChannelFsm(FsmBuilder builder, State initialState) { - builder.addTransitionAction(new TransitionAction() { - @Override - public void execute(ActionContext context) { - transitionListeners.forEach( - listener -> - listener.onStateTransition(context.from(), context.to(), context.event()) - ); - } - - @Override - public boolean matches(State from, State to, Event event) { - return true; - } - }); - - this.fsm = builder.build(initialState); + private final List transitionListeners = new CopyOnWriteArrayList<>(); + + private final Fsm fsm; + + ChannelFsm(FsmBuilder builder, State initialState) { + builder.addTransitionAction(new TransitionAction() { + @Override + public void execute(ActionContext context) { + transitionListeners.forEach( + listener -> + listener.onStateTransition(context.from(), context.to(), context.event()) + ); + } + + @Override + public boolean matches(State from, State to, Event event) { + return true; + } + }); + + this.fsm = builder.build(initialState); + } + + public Fsm getFsm() { + return fsm; + } + + /** + * Fire a {@link Connect} event and return a {@link CompletableFuture} that completes successfully + * with the {@link Channel} if a successful connection is made, or already exists, and completes + * exceptionally otherwise. + * + * @return a {@link CompletableFuture} that completes successfully with the {@link Channel} if a + * successful connection was made, or already exists, and completes exceptionally otherwise. + */ + public CompletableFuture connect() { + Connect connect = new Connect(); + + fsm.fireEvent(connect); + + return connect.channelFuture; + } + + /** + * Fire a {@link Disconnect} event and return a {@link CompletableFuture} that completes + * successfully when the {@link Channel} has been closed. + * + * @return a {@link CompletableFuture} that completes successfully when the {@link Channel} has + * been closed. + */ + public CompletableFuture disconnect() { + Disconnect disconnect = new Disconnect(); + + fsm.fireEvent(disconnect); + + return disconnect.disconnectFuture; + } + + /** + * Fire a {@link GetChannel} event and return a {@link CompletableFuture} that completes + * successfully when the {@link Channel} is available and completes exceptionally if the FSM is + * not currently connected or the connection attempt failed. + * + *

{@link #connect()} must have been called at least once before attempting to get a Channel. + * Whether further calls are necessary depends on whether the FSM is configured to be persistent + * in its connection attempts or not. + * + *

The returned CompletableFuture always fails exceptionally if the FSM is not connected. + * + *

This method is equivalent to {@code getChannel(true)} - if the state machine is + * reconnecting it will wait for the result. + * + * @return a {@link CompletableFuture} that completes successfully when the {@link Channel} is + * available and completes exceptionally if the FSM is not currently connected or the + * connection attempt failed. + */ + public CompletableFuture getChannel() { + return getChannel(true); + } + + /** + * Fire a {@link GetChannel} event and return a {@link CompletableFuture} that completes + * successfully when the {@link Channel} is available and completes exceptionally if the FSM is + * not currently connected or the connection attempt failed. + * + *

{@link #connect()} must have been called at least once before attempting to get a Channel. + * Whether further calls are necessary depends on whether the FSM is configured to be persistent + * in its connection attempts or not. + * + *

The returned CompletableFuture always fails exceptionally if the FSM is not connected. + * + * @param waitForReconnect when {@code true} and the state machine is in + * {@link State#ReconnectWait} the future will not be completed until the result of the + * subsequent reconnect attempt has been obtained. When {@code false} and the state machine is + * in {@link State#ReconnectWait} the future is failed immediately. This parameter has no + * effect in other states. + * @return a {@link CompletableFuture} that completes successfully when the {@link Channel} is + * available and *completes exceptionally if the FSM is not currently connected or the + * connection attempt failed. + */ + public CompletableFuture getChannel(boolean waitForReconnect) { + CompletableFuture future = fsm.getFromContext(ctx -> { + State state = ctx.currentState(); + + if (state == State.Connected) { + ConnectFuture cf = KEY_CF.get(ctx); + + assert cf != null; + + return cf.future; + } else { + return null; + } + }); + + if (future != null) { + return future; + } else { + // "Slow" path... not connected yet. + GetChannel getChannel = new GetChannel(waitForReconnect); + + fsm.fireEvent(getChannel); + + return getChannel.channelFuture; } + } - public Fsm getFsm() { - return fsm; - } + /** + * Get the current {@link State} of the {@link ChannelFsm}. + * + * @return the current {@link State} of the {@link ChannelFsm}. + */ + public State getState() { + return fsm.getFromContext(FsmContext::currentState); + } - /** - * Fire a {@link Connect} event and return a {@link CompletableFuture} that completes successfully with the - * {@link Channel} if a successful connection is made, or already exists, and completes exceptionally otherwise. - * - * @return a {@link CompletableFuture} that completes successfully with the {@link Channel} if a successful - * connection was made, or already exists, and completes exceptionally otherwise. - */ - public CompletableFuture connect() { - Connect connect = new Connect(); + /** + * Add a {@link TransitionListener}. + * + * @param transitionListener the {@link TransitionListener}. + */ + public void addTransitionListener(TransitionListener transitionListener) { + transitionListeners.add(transitionListener); + } - fsm.fireEvent(connect); + /** + * Remove a previously registered {@link TransitionListener}. + * + * @param transitionListener the {@link TransitionListener}. + */ + public void removeTransitionListener(TransitionListener transitionListener) { + transitionListeners.remove(transitionListener); + } - return connect.channelFuture; - } + static final FsmContext.Key KEY_CF = + new FsmContext.Key<>("connectFuture", ConnectFuture.class); - /** - * Fire a {@link Disconnect} event and return a {@link CompletableFuture} that completes successfully when the - * {@link Channel} has been closed. - * - * @return a {@link CompletableFuture} that completes successfully when the {@link Channel} has been closed. - */ - public CompletableFuture disconnect() { - Disconnect disconnect = new Disconnect(); + static final FsmContext.Key KEY_DF = + new FsmContext.Key<>("disconnectFuture", DisconnectFuture.class); - fsm.fireEvent(disconnect); + static final FsmContext.Key KEY_RD = + new FsmContext.Key<>("reconnectDelay", Long.class); - return disconnect.disconnectFuture; - } + static final FsmContext.Key KEY_RDF = + new FsmContext.Key<>("reconnectDelayCancellable", Cancellable.class); - /** - * Fire a {@link GetChannel} event and return a {@link CompletableFuture} that completes successfully when the - * {@link Channel} is available and completes exceptionally if the FSM is not currently connected or the connection - * attempt failed. - *

- * {@link #connect()} must have been called at least once before attempting to get a Channel. Whether further calls - * are necessary depends on whether the FSM is configured to be persistent in its connection attempts or not. - *

- * The returned CompletableFuture always fails exceptionally if the FSM is not connected. - *

- * This method is equivalent to {@code getChannel(true)} - if the state machine is reconnecting it will wait for - * the result. - * - * @return a {@link CompletableFuture} that completes successfully when the {@link Channel} is available and - * completes exceptionally if the FSM is not currently connected or the connection attempt failed. - */ - public CompletableFuture getChannel() { - return getChannel(true); - } + static class ConnectFuture { - /** - * Fire a {@link GetChannel} event and return a {@link CompletableFuture} that completes successfully when the - * {@link Channel} is available and completes exceptionally if the FSM is not currently connected or the connection - * attempt failed. - *

- * {@link #connect()} must have been called at least once before attempting to get a Channel. Whether further calls - * are necessary depends on whether the FSM is configured to be persistent in its connection attempts or not. - *

- * The returned CompletableFuture always fails exceptionally if the FSM is not connected. - * - * @param waitForReconnect when {@code true} and the state machine is in {@link State#ReconnectWait} the future - * will not be completed until the result of the subsequent reconnect attempt has been - * obtained. When {@code false} and the state machine is in {@link State#ReconnectWait} - * the future is failed immediately. This parameter has no effect in other states. - * @return a {@link CompletableFuture} that completes successfully when the {@link Channel} is available and - * *completes exceptionally if the FSM is not currently connected or the connection attempt failed. - */ - public CompletableFuture getChannel(boolean waitForReconnect) { - CompletableFuture future = fsm.getFromContext(ctx -> { - State state = ctx.currentState(); - - if (state == State.Connected) { - ConnectFuture cf = KEY_CF.get(ctx); - - assert cf != null; + final CompletableFuture future = new CompletableFuture<>(); + } - return cf.future; - } else { - return null; - } - }); + static class DisconnectFuture { - if (future != null) { - return future; - } else { - // "Slow" path... not connected yet. - GetChannel getChannel = new GetChannel(waitForReconnect); + final CompletableFuture future = new CompletableFuture<>(); + } - fsm.fireEvent(getChannel); - - return getChannel.channelFuture; - } - } + public interface TransitionListener { /** - * Get the current {@link State} of the {@link ChannelFsm}. + * A state transition has occurred. * - * @return the current {@link State} of the {@link ChannelFsm}. - */ - public State getState() { - return fsm.getFromContext(FsmContext::currentState); - } - - /** - * Add a {@link TransitionListener}. + *

Transitions may be internal, i.e. the {@code from} and {@code to} state are the same. * - * @param transitionListener the {@link TransitionListener}. - */ - public void addTransitionListener(TransitionListener transitionListener) { - transitionListeners.add(transitionListener); - } - - /** - * Remove a previously registered {@link TransitionListener}. + *

Listener notification is implemented as a {@link TransitionAction}, so take care not to + * block in this callback as it will block the state machine evaluation as well. * - * @param transitionListener the {@link TransitionListener}. + * @param from the {@link State} transitioned from. + * @param to the {@link State} transitioned to. + * @param via the {@link Event} that caused the transition. */ - public void removeTransitionListener(TransitionListener transitionListener) { - transitionListeners.remove(transitionListener); - } - - static final FsmContext.Key KEY_CF = - new FsmContext.Key<>("connectFuture", ConnectFuture.class); - - static final FsmContext.Key KEY_DF = - new FsmContext.Key<>("disconnectFuture", DisconnectFuture.class); - - static final FsmContext.Key KEY_RD = - new FsmContext.Key<>("reconnectDelay", Long.class); - - static final FsmContext.Key KEY_RDF = - new FsmContext.Key<>("reconnectDelayCancellable", Cancellable.class); + void onStateTransition(State from, State to, Event via); - static class ConnectFuture { - final CompletableFuture future = new CompletableFuture<>(); - } - - static class DisconnectFuture { - final CompletableFuture future = new CompletableFuture<>(); - } - - public interface TransitionListener { - - /** - * A state transition has occurred. - *

- * Transitions may be internal, i.e. the {@code from} and {@code to} state are the same. - *

- * Listener notification is implemented as a {@link TransitionAction}, so take care not to - * block in this callback as it will block the state machine evaluation as well. - * - * @param from the {@link State} transitioned from. - * @param to the {@link State} transitioned to. - * @param via the {@link Event} that caused the transition. - */ - void onStateTransition(State from, State to, Event via); - - } + } } diff --git a/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmConfig.java b/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmConfig.java index 3d536ec..7944667 100644 --- a/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmConfig.java +++ b/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmConfig.java @@ -1,111 +1,98 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm; -import java.util.Map; import java.util.concurrent.Executor; public interface ChannelFsmConfig { - /** - * {@code true} if the ChannelFsm should be lazy, i.e. after an unintentional channel disconnect it waits in an - * Idle state until the Channel is requested via {@link ChannelFsm#connect()} or {@link ChannelFsm#getChannel()}. - * - * @return {@code true} if the ChannelFsm should be lazy, - */ - boolean isLazy(); - - /** - * {@code true} if the ChannelFsm should be persistent in its initial connect attempt, i.e. if the initial attempt - * to connect initiated by {@link ChannelFsm#connect()}} fails, it will immediately move into a reconnecting state - * and continue to try and establish a connection. - *

- * Each time a connection attempt fails, including the first, the outstanding - * {@link java.util.concurrent.CompletableFuture}s will be completed exceptionally. - * - * @return {@code true} if the ChannelFsm should be persistent in its initial connect attempt. - */ - boolean isPersistent(); + /** + * {@code true} if the ChannelFsm should be lazy, i.e. after an unintentional channel disconnect + * it waits in an Idle state until the Channel is requested via {@link ChannelFsm#connect()} or + * {@link ChannelFsm#getChannel()}. + * + * @return {@code true} if the ChannelFsm should be lazy, + */ + boolean isLazy(); - /** - * Get the maximum amount of time, in seconds, before a keep alive occurs on an idle channel. - *

- * An idle channel is one that that hasn't read any bytes within the time defined by this value. - *

- * Return 0 to disable keep alives. - * - * @return the maximum amount of time, in seconds, before a keep alive occurs on an idle channel. - */ - int getMaxIdleSeconds(); + /** + * {@code true} if the ChannelFsm should be persistent in its initial connect attempt, i.e. if the + * initial attempt to connect initiated by {@link ChannelFsm#connect()}} fails, it will + * immediately move into a reconnecting state and continue to try and establish a connection. + * + *

Each time a connection attempt fails, including the first, the outstanding + * {@link java.util.concurrent.CompletableFuture}s will be completed exceptionally. + * + * @return {@code true} if the ChannelFsm should be persistent in its initial connect attempt. + */ + boolean isPersistent(); - /** - * Get the maximum delay to occur between reconnect attempts. Will be rounded up to the nearest power of 2. - *

- * The delay is increased exponentially starting at 1 second until the maximum delay, e.g. - * (1, 2, 4, 8, 16, 32, 32, 32, 32...). - * - * @return the maximum delay to occur between reconnect attempts. - */ - int getMaxReconnectDelaySeconds(); + /** + * Get the maximum amount of time, in seconds, before a keep alive occurs on an idle channel. + * + *

An idle channel is one that that hasn't read any bytes within the time defined by this + * value. + * + *

Return 0 to disable keep alives. + * + * @return the maximum amount of time, in seconds, before a keep alive occurs on an idle channel. + */ + int getMaxIdleSeconds(); - /** - * Get the {@link ChannelActions} delegate. - * - * @return the {@link ChannelActions} delegate. - */ - ChannelActions getChannelActions(); + /** + * Get the maximum delay to occur between reconnect attempts. Will be rounded up to the nearest + * power of 2. + * + *

The delay is increased exponentially starting at 1 second until the maximum delay, e.g. + * (1, 2, 4, 8, 16, 32, 32, 32, 32...). + * + * @return the maximum delay to occur between reconnect attempts. + */ + int getMaxReconnectDelaySeconds(); - /** - * Get the {@link Executor} to use. - * - * @return the {@link Executor} to use. - */ - Executor getExecutor(); + /** + * Get the {@link ChannelActions} delegate. + * + * @return the {@link ChannelActions} delegate. + */ + ChannelActions getChannelActions(); - /** - * Get the {@link Scheduler} to use. - * - * @return the {@link Scheduler} to use. - */ - Scheduler getScheduler(); + /** + * Get the {@link Executor} to use. + * + * @return the {@link Executor} to use. + */ + Executor getExecutor(); - /** - * Get the logger name the FSM should use. - * - * @return the logger name the FSM should use. - */ - String getLoggerName(); + /** + * Get the {@link Scheduler} to use. + * + * @return the {@link Scheduler} to use. + */ + Scheduler getScheduler(); - /** - * Get the logging context Map a {@link ChannelFsm} instance will use. - *

- * Keys and values in the Map will be set on the SLF4J {@link org.slf4j.MDC} when logging. - * - * @return the logging context Map a {@link ChannelFsm} instance will use. - */ - Map getLoggingContext(); + /** + * Get the user-configurable context associated with this ChannelFsm. + * + * @return the user-configurable context associated with this ChannelFsm. + */ + Object getContext(); - /** - * Create a new {@link ChannelFsmConfigBuilder}. - * - * @return a new {@link ChannelFsmConfigBuilder}. - */ - static ChannelFsmConfigBuilder newBuilder() { - return new ChannelFsmConfigBuilder(); - } + /** + * Create a new {@link ChannelFsmConfigBuilder}. + * + * @return a new {@link ChannelFsmConfigBuilder}. + */ + static ChannelFsmConfigBuilder newBuilder() { + return new ChannelFsmConfigBuilder(); + } } diff --git a/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmConfigBuilder.java b/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmConfigBuilder.java index 840841b..a2c57af 100644 --- a/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmConfigBuilder.java +++ b/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmConfigBuilder.java @@ -1,24 +1,15 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -26,243 +17,218 @@ public class ChannelFsmConfigBuilder { - static int DEFAULT_MAX_RECONNECT_DELAY_SECONDS = 32; - - private boolean lazy = false; - private boolean persistent = true; - private int maxIdleSeconds = 15; - private int maxReconnectDelaySeconds = DEFAULT_MAX_RECONNECT_DELAY_SECONDS; - private ChannelActions channelActions; - private Executor executor; - private Scheduler scheduler; - private String loggerName; - private Map loggingContext = Collections.emptyMap(); - - /** - * @param lazy {@code true} if the ChannelFsm should be lazy, - * @return this {@link ChannelFsmConfigBuilder}. - * @see ChannelFsmConfig#isLazy() - */ - public ChannelFsmConfigBuilder setLazy(boolean lazy) { - this.lazy = lazy; - return this; + static int DEFAULT_MAX_RECONNECT_DELAY_SECONDS = 32; + + private boolean lazy = false; + private boolean persistent = true; + private int maxIdleSeconds = 15; + private int maxReconnectDelaySeconds = DEFAULT_MAX_RECONNECT_DELAY_SECONDS; + private ChannelActions channelActions; + private Executor executor; + private Scheduler scheduler; + private Object context; + + /** + * @param lazy {@code true} if the ChannelFsm should be lazy, + * @return this {@link ChannelFsmConfigBuilder}. + * @see ChannelFsmConfig#isLazy() + */ + public ChannelFsmConfigBuilder setLazy(boolean lazy) { + this.lazy = lazy; + return this; + } + + /** + * @param persistent {@code true} if the ChannelFsm should be persistent in its initial + * connect attempt. + * @return this {@link ChannelFsmConfigBuilder}. + * @see ChannelFsmConfig#isPersistent() + */ + public ChannelFsmConfigBuilder setPersistent(boolean persistent) { + this.persistent = persistent; + return this; + } + + /** + * @param maxIdleSeconds the maximum amount of time, in seconds, before a keep alive occurs on + * an idle channel. + * @return this {@link ChannelFsmConfigBuilder}. + * @see ChannelFsmConfig#getMaxIdleSeconds() + */ + public ChannelFsmConfigBuilder setMaxIdleSeconds(int maxIdleSeconds) { + this.maxIdleSeconds = maxIdleSeconds; + return this; + } + + /** + * @param maxReconnectDelaySeconds the maximum delay to occur between reconnect attempts. + * @return this {@link ChannelFsmConfigBuilder}. + * @see ChannelFsmConfig#getMaxReconnectDelaySeconds() + */ + public ChannelFsmConfigBuilder setMaxReconnectDelaySeconds(int maxReconnectDelaySeconds) { + this.maxReconnectDelaySeconds = maxReconnectDelaySeconds; + return this; + } + + /** + * @param channelActions the {@link ChannelActions} delegate. + * @return this {@link ChannelFsmConfigBuilder}. + * @see ChannelFsmConfig#getChannelActions() + */ + public ChannelFsmConfigBuilder setChannelActions(ChannelActions channelActions) { + this.channelActions = channelActions; + return this; + } + + /** + * @param executor the {@link Executor} to use. + * @return this {@link ChannelFsmConfigBuilder}. + * @see ChannelFsmConfig#getExecutor() + */ + public ChannelFsmConfigBuilder setExecutor(Executor executor) { + this.executor = executor; + return this; + } + + /** + * @param scheduler the {@link Scheduler} to use. + * @return this {@link ChannelFsmConfigBuilder}. + * @see ChannelFsmConfig#getScheduler() + */ + public ChannelFsmConfigBuilder setScheduler(Scheduler scheduler) { + this.scheduler = scheduler; + return this; + } + + /** + * @param scheduledExecutor the {@link ScheduledExecutorService} to use. + * @return this {@link ChannelFsmConfigBuilder}. + * @see ChannelFsmConfig#getScheduler() + */ + public ChannelFsmConfigBuilder setScheduler(ScheduledExecutorService scheduledExecutor) { + this.scheduler = Scheduler.fromScheduledExecutor(scheduledExecutor); + return this; + } + + /** + * @param context the user-configurable context associated with this ChannelFsm. + * @return this {@link ChannelFsmConfigBuilder}. + * @see ChannelFsmConfig#getContext() + */ + public ChannelFsmConfigBuilder setContext(Object context) { + this.context = context; + return this; + } + + public ChannelFsmConfig build() { + if (channelActions == null) { + throw new IllegalArgumentException("channelActions must be non-null"); } - - /** - * @param persistent {@code true} if the ChannelFsm should be persistent in its initial connect attempt. - * @return this {@link ChannelFsmConfigBuilder}. - * @see ChannelFsmConfig#isPersistent() - */ - public ChannelFsmConfigBuilder setPersistent(boolean persistent) { - this.persistent = persistent; - return this; + if (maxReconnectDelaySeconds < 1) { + maxReconnectDelaySeconds = DEFAULT_MAX_RECONNECT_DELAY_SECONDS; } - - /** - * @param maxIdleSeconds the maximum amount of time, in seconds, before a keep alive occurs on an idle channel. - * @return this {@link ChannelFsmConfigBuilder}. - * @see ChannelFsmConfig#getMaxIdleSeconds() - */ - public ChannelFsmConfigBuilder setMaxIdleSeconds(int maxIdleSeconds) { - this.maxIdleSeconds = maxIdleSeconds; - return this; + if (executor == null) { + executor = SharedExecutor.INSTANCE; } - - /** - * @param maxReconnectDelaySeconds the maximum delay to occur between reconnect attempts. - * @return this {@link ChannelFsmConfigBuilder}. - * @see ChannelFsmConfig#getMaxReconnectDelaySeconds() - */ - public ChannelFsmConfigBuilder setMaxReconnectDelaySeconds(int maxReconnectDelaySeconds) { - this.maxReconnectDelaySeconds = maxReconnectDelaySeconds; - return this; + if (scheduler == null) { + scheduler = SharedScheduler.INSTANCE; } - /** - * @param channelActions the {@link ChannelActions} delegate. - * @return this {@link ChannelFsmConfigBuilder}. - * @see ChannelFsmConfig#getChannelActions() - */ - public ChannelFsmConfigBuilder setChannelActions(ChannelActions channelActions) { - this.channelActions = channelActions; - return this; + return new ChannelFsmConfigImpl( + lazy, + persistent, + maxIdleSeconds, + maxReconnectDelaySeconds, + channelActions, + executor, + scheduler, + context + ); + } + + private static class SharedExecutor { + + private static final ExecutorService INSTANCE = Executors.newSingleThreadExecutor(); + } + + private static class SharedScheduler { + + private static final Scheduler INSTANCE = + Scheduler.fromScheduledExecutor(Executors.newSingleThreadScheduledExecutor()); + } + + private static class ChannelFsmConfigImpl implements ChannelFsmConfig { + + private final boolean lazy; + private final boolean persistent; + private final int maxIdleSeconds; + private final int maxReconnectDelaySeconds; + private final ChannelActions channelActions; + private final Executor executor; + private final Scheduler scheduler; + private final Object context; + + ChannelFsmConfigImpl( + boolean lazy, + boolean persistent, + int maxIdleSeconds, + int maxReconnectDelaySeconds, + ChannelActions channelActions, + Executor executor, + Scheduler scheduler, + Object context + ) { + + this.lazy = lazy; + this.persistent = persistent; + this.maxIdleSeconds = maxIdleSeconds; + this.maxReconnectDelaySeconds = maxReconnectDelaySeconds; + this.channelActions = channelActions; + this.executor = executor; + this.scheduler = scheduler; + this.context = context; } - /** - * @param executor the {@link Executor} to use. - * @return this {@link ChannelFsmConfigBuilder}. - * @see ChannelFsmConfig#getExecutor() - */ - public ChannelFsmConfigBuilder setExecutor(Executor executor) { - this.executor = executor; - return this; + @Override + public boolean isLazy() { + return lazy; } - /** - * @param scheduler the {@link Scheduler} to use. - * @return this {@link ChannelFsmConfigBuilder}. - * @see ChannelFsmConfig#getScheduler() - */ - public ChannelFsmConfigBuilder setScheduler(Scheduler scheduler) { - this.scheduler = scheduler; - return this; + @Override + public boolean isPersistent() { + return persistent; } - /** - * @param scheduledExecutor the {@link ScheduledExecutorService} to use. - * @return this {@link ChannelFsmConfigBuilder}. - * @see ChannelFsmConfig#getScheduler() - */ - public ChannelFsmConfigBuilder setScheduler(ScheduledExecutorService scheduledExecutor) { - this.scheduler = Scheduler.fromScheduledExecutor(scheduledExecutor); - return this; + @Override + public int getMaxIdleSeconds() { + return maxIdleSeconds; } - /** - * @param loggerName the logger name the FSM should use. - * @return this {@link ChannelFsmConfigBuilder}. - * @see ChannelFsmConfig#getLoggerName() - */ - public ChannelFsmConfigBuilder setLoggerName(String loggerName) { - this.loggerName = loggerName; - return this; + @Override + public int getMaxReconnectDelaySeconds() { + return maxReconnectDelaySeconds; } - /** - * Set the logging context Map a {@link ChannelFsm} instance will use. - *

- * Keys and values in the Map will be set on the SLF4J {@link org.slf4j.MDC} when logging. - *

- * This method makes a defensive copy of {@code loggingContext}. - * - * @param loggingContext the logging context Map a {@link ChannelFsm} instance will use. - * @return this {@link ChannelFsmConfigBuilder} - * @see ChannelFsmConfig#getLoggingContext() - */ - public ChannelFsmConfigBuilder setLoggingContext(Map loggingContext) { - this.loggingContext = new ConcurrentHashMap<>(loggingContext); - return this; + @Override + public ChannelActions getChannelActions() { + return channelActions; } - public ChannelFsmConfig build() { - if (channelActions == null) { - throw new IllegalArgumentException("channelActions must be non-null"); - } - if (maxReconnectDelaySeconds < 1) { - maxReconnectDelaySeconds = DEFAULT_MAX_RECONNECT_DELAY_SECONDS; - } - if (executor == null) { - executor = SharedExecutor.INSTANCE; - } - if (scheduler == null) { - scheduler = SharedScheduler.INSTANCE; - } - if (loggerName == null) { - loggerName = ChannelFsm.class.getName(); - } - - return new ChannelFsmConfigImpl( - lazy, - persistent, - maxIdleSeconds, - maxReconnectDelaySeconds, - channelActions, - executor, - scheduler, - loggerName, - loggingContext - ); + @Override + public Executor getExecutor() { + return executor; } - private static class SharedExecutor { - private static final ExecutorService INSTANCE = Executors.newSingleThreadExecutor(); + @Override + public Scheduler getScheduler() { + return scheduler; } - private static class SharedScheduler { - private static final Scheduler INSTANCE = - Scheduler.fromScheduledExecutor(Executors.newSingleThreadScheduledExecutor()); + @Override + public Object getContext() { + return context; } - private static class ChannelFsmConfigImpl implements ChannelFsmConfig { - - private final boolean lazy; - private final boolean persistent; - private final int maxIdleSeconds; - private final int maxReconnectDelaySeconds; - private final ChannelActions channelActions; - private final Executor executor; - private final Scheduler scheduler; - private final String loggerName; - private final Map loggingContext; - - ChannelFsmConfigImpl( - boolean lazy, - boolean persistent, - int maxIdleSeconds, - int maxReconnectDelaySeconds, - ChannelActions channelActions, - Executor executor, - Scheduler scheduler, - String loggerName, - Map loggingContext - ) { - - this.lazy = lazy; - this.persistent = persistent; - this.maxIdleSeconds = maxIdleSeconds; - this.maxReconnectDelaySeconds = maxReconnectDelaySeconds; - this.channelActions = channelActions; - this.executor = executor; - this.scheduler = scheduler; - this.loggerName = loggerName; - this.loggingContext = loggingContext; - } - - @Override - public boolean isLazy() { - return lazy; - } - - @Override - public boolean isPersistent() { - return persistent; - } - - @Override - public int getMaxIdleSeconds() { - return maxIdleSeconds; - } - - @Override - public int getMaxReconnectDelaySeconds() { - return maxReconnectDelaySeconds; - } - - @Override - public ChannelActions getChannelActions() { - return channelActions; - } - - @Override - public Executor getExecutor() { - return executor; - } - - @Override - public Scheduler getScheduler() { - return scheduler; - } - - @Override - public String getLoggerName() { - return loggerName; - } - - @Override - public Map getLoggingContext() { - return loggingContext; - } - - } + } } diff --git a/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmFactory.java b/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmFactory.java index fb629d9..52eb258 100644 --- a/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmFactory.java +++ b/src/main/java/com/digitalpetri/netty/fsm/ChannelFsmFactory.java @@ -1,612 +1,633 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; +import static com.digitalpetri.netty.fsm.ChannelFsm.KEY_CF; +import static com.digitalpetri.netty.fsm.ChannelFsm.KEY_DF; +import static com.digitalpetri.netty.fsm.ChannelFsm.KEY_RD; +import static com.digitalpetri.netty.fsm.ChannelFsm.KEY_RDF; +import static com.digitalpetri.netty.fsm.CompletionBuilders.completeAsync; +import com.digitalpetri.fsm.FsmContext; +import com.digitalpetri.fsm.Log; +import com.digitalpetri.fsm.dsl.ActionContext; +import com.digitalpetri.fsm.dsl.FsmBuilder; import com.digitalpetri.netty.fsm.ChannelFsm.ConnectFuture; import com.digitalpetri.netty.fsm.ChannelFsm.DisconnectFuture; import com.digitalpetri.netty.fsm.Scheduler.Cancellable; -import com.digitalpetri.strictmachine.FsmContext; -import com.digitalpetri.strictmachine.dsl.ActionContext; -import com.digitalpetri.strictmachine.dsl.FsmBuilder; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.IdleStateHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.MDC; - -import static com.digitalpetri.netty.fsm.ChannelFsm.KEY_CF; -import static com.digitalpetri.netty.fsm.ChannelFsm.KEY_DF; -import static com.digitalpetri.netty.fsm.ChannelFsm.KEY_RD; -import static com.digitalpetri.netty.fsm.ChannelFsm.KEY_RDF; -import static com.digitalpetri.netty.fsm.util.CompletionBuilders.completeAsync; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; public class ChannelFsmFactory { - private final ChannelFsmConfig config; - - public ChannelFsmFactory(ChannelFsmConfig config) { - this.config = config; - } - - /** - * Create a new {@link ChannelFsm} instance. - * - * @return a new {@link ChannelFsm} instance. - */ - public ChannelFsm newChannelFsm() { - return newChannelFsm(State.NotConnected); - } - - ChannelFsm newChannelFsm(State initialState) { - FsmBuilder builder = new FsmBuilder<>( - config.getExecutor(), - config.getLoggerName(), - config.getLoggingContext() - ); - - configureChannelFsm(builder, config); - - return new ChannelFsm(builder, initialState); - } - - /** - * Create a new {@link ChannelFsm} instance from {@code config}. - * - * @param config a {@link ChannelFsmConfig}. - * @return a new {@link ChannelFsm} from {@code config}. - */ - public static ChannelFsm newChannelFsm(ChannelFsmConfig config) { - return new ChannelFsmFactory(config).newChannelFsm(); - } - - private static void configureChannelFsm(FsmBuilder fb, ChannelFsmConfig config) { - configureNotConnectedState(fb, config); - configureIdleState(fb, config); - configureConnectingState(fb, config); - configureConnectedState(fb, config); - configureDisconnectingState(fb, config); - configureReconnectWaitState(fb, config); - configureReconnectingState(fb, config); - } - - private static void configureNotConnectedState(FsmBuilder fb, ChannelFsmConfig config) { - fb.when(State.NotConnected) - .on(Event.Connect.class) - .transitionTo(State.Connecting); - - fb.onInternalTransition(State.NotConnected) - .via(Event.Disconnect.class) - .execute(ctx -> { - Event.Disconnect disconnectEvent = (Event.Disconnect) ctx.event(); - - config.getExecutor().execute(() -> - disconnectEvent.disconnectFuture.complete(null) - ); - }); - - fb.onInternalTransition(State.NotConnected) - .via(Event.GetChannel.class) - .execute(ctx -> { - Event.GetChannel getChannelEvent = (Event.GetChannel) ctx.event(); - - config.getExecutor().execute(() -> - getChannelEvent.channelFuture - .completeExceptionally(new Exception("not connected")) - ); - }); - } - - private static void configureIdleState(FsmBuilder fb, ChannelFsmConfig config) { - fb.when(State.Idle) - .on(Event.Connect.class) - .transitionTo(State.Reconnecting); - - fb.when(State.Idle) - .on(Event.GetChannel.class) - .transitionTo(State.Reconnecting); - - fb.when(State.Idle) - .on(Event.Disconnect.class) - .transitionTo(State.NotConnected); - - fb.onTransitionFrom(State.Idle) - .to(State.NotConnected) - .via(Event.Disconnect.class) - .execute(ctx -> { - Event.Disconnect disconnect = (Event.Disconnect) ctx.event(); - config.getExecutor().execute(() -> - disconnect.disconnectFuture.complete(null) - ); - }); - } - - private static void configureConnectingState(FsmBuilder fb, ChannelFsmConfig config) { - if (config.isPersistent()) { - if (config.isLazy()) { - fb.when(State.Connecting) - .on(Event.ConnectFailure.class) - .transitionTo(State.Idle); - } else { - fb.when(State.Connecting) - .on(Event.ConnectFailure.class) - .transitionTo(State.ReconnectWait); - } - } else { - fb.when(State.Connecting) - .on(Event.ConnectFailure.class) - .transitionTo(State.NotConnected); - } - + private final ChannelFsmConfig config; + + public ChannelFsmFactory(ChannelFsmConfig config) { + this.config = config; + } + + /** + * Create a new {@link ChannelFsm} instance. + * + * @return a new {@link ChannelFsm} instance. + */ + public ChannelFsm newChannelFsm() { + return newChannelFsm(State.NotConnected); + } + + ChannelFsm newChannelFsm(State initialState) { + var builder = new FsmBuilder(config.getContext(), config.getExecutor()); + + configureChannelFsm(builder, config); + + return new ChannelFsm(builder, initialState); + } + + /** + * Create a new {@link ChannelFsm} instance from {@code config}. + * + * @param config a {@link ChannelFsmConfig}. + * @return a new {@link ChannelFsm} from {@code config}. + */ + public static ChannelFsm newChannelFsm(ChannelFsmConfig config) { + return new ChannelFsmFactory(config).newChannelFsm(); + } + + private static void configureChannelFsm(FsmBuilder fb, ChannelFsmConfig config) { + configureNotConnectedState(fb, config); + configureIdleState(fb, config); + configureConnectingState(fb, config); + configureConnectedState(fb, config); + configureDisconnectingState(fb, config); + configureReconnectWaitState(fb, config); + configureReconnectingState(fb, config); + } + + private static void configureNotConnectedState( + FsmBuilder fb, + ChannelFsmConfig config + ) { + + fb.when(State.NotConnected) + .on(Event.Connect.class) + .transitionTo(State.Connecting); + + fb.onInternalTransition(State.NotConnected) + .via(Event.Disconnect.class) + .execute(ctx -> { + Event.Disconnect disconnectEvent = (Event.Disconnect) ctx.event(); + + config.getExecutor().execute(() -> + disconnectEvent.disconnectFuture.complete(null) + ); + }); + + fb.onInternalTransition(State.NotConnected) + .via(Event.GetChannel.class) + .execute(ctx -> { + Event.GetChannel getChannelEvent = (Event.GetChannel) ctx.event(); + + config.getExecutor().execute(() -> + getChannelEvent.channelFuture + .completeExceptionally(new Exception("not connected")) + ); + }); + } + + private static void configureIdleState(FsmBuilder fb, ChannelFsmConfig config) { + fb.when(State.Idle) + .on(Event.Connect.class) + .transitionTo(State.Reconnecting); + + fb.when(State.Idle) + .on(Event.GetChannel.class) + .transitionTo(State.Reconnecting); + + fb.when(State.Idle) + .on(Event.Disconnect.class) + .transitionTo(State.NotConnected); + + fb.onTransitionFrom(State.Idle) + .to(State.NotConnected) + .via(Event.Disconnect.class) + .execute(ctx -> { + Event.Disconnect disconnect = (Event.Disconnect) ctx.event(); + config.getExecutor().execute(() -> + disconnect.disconnectFuture.complete(null) + ); + }); + } + + private static void configureConnectingState( + FsmBuilder fb, + ChannelFsmConfig config + ) { + + if (config.isPersistent()) { + if (config.isLazy()) { fb.when(State.Connecting) - .on(Event.ConnectSuccess.class) - .transitionTo(State.Connected); - - fb.onTransitionTo(State.Connecting) - .from(s -> s != State.Connecting) - .via(e -> e.getClass() == Event.Connect.class) - .execute(ctx -> { - ConnectFuture cf = new ConnectFuture(); - KEY_CF.set(ctx, cf); - - handleConnectEvent(ctx, config); - - connect(ctx, config); - }); - - fb.onInternalTransition(State.Connecting) - .via(Event.Connect.class) - .execute(ctx -> handleConnectEvent(ctx, config)); - - fb.onInternalTransition(State.Connecting) - .via(Event.GetChannel.class) - .execute(ctx -> handleGetChannelEvent(ctx, config)); - - fb.onInternalTransition(State.Connecting) - .via(Event.Disconnect.class) - .execute(ctx -> ctx.shelveEvent(ctx.event())); - - fb.onTransitionFrom(State.Connecting) - .to(s -> s != State.Connecting) - .viaAny() - .execute(FsmContext::processShelvedEvents); - - fb.onTransitionFrom(State.Connecting) - .to(s -> s != State.Connecting) - .via(Event.ConnectFailure.class) - .execute(ctx -> handleConnectFailureEvent(ctx, config)); + .on(Event.ConnectFailure.class) + .transitionTo(State.Idle); + } else { + fb.when(State.Connecting) + .on(Event.ConnectFailure.class) + .transitionTo(State.ReconnectWait); + } + } else { + fb.when(State.Connecting) + .on(Event.ConnectFailure.class) + .transitionTo(State.NotConnected); } - private static void configureConnectedState(FsmBuilder fb, ChannelFsmConfig config) { - Logger logger = LoggerFactory.getLogger(config.getLoggerName()); - - fb.when(State.Connected) - .on(Event.Disconnect.class) - .transitionTo(State.Disconnecting); - - if (config.isLazy()) { - fb.when(State.Connected) - .on(e -> - e.getClass() == Event.ChannelInactive.class || - e.getClass() == Event.KeepAliveFailure.class) - .transitionTo(State.Idle); - } else { - fb.when(State.Connected) - .on(e -> - e.getClass() == Event.ChannelInactive.class || - e.getClass() == Event.KeepAliveFailure.class) - .transitionTo(State.ReconnectWait); - } - - fb.onTransitionTo(State.Connected) - .from(s -> s != State.Connected) - .via(Event.ConnectSuccess.class) - .execute(ctx -> { - Event.ConnectSuccess event = (Event.ConnectSuccess) ctx.event(); - Channel channel = event.channel; - - if (config.getMaxIdleSeconds() > 0) { - channel.pipeline().addFirst(new IdleStateHandler(config.getMaxIdleSeconds(), 0, 0)); - } - - channel.pipeline().addLast(new ChannelInboundHandlerAdapter() { - @Override - public void channelInactive(ChannelHandlerContext channelContext) throws Exception { - config.getLoggingContext().forEach(MDC::put); - try { - logger.debug( - "[{}] channelInactive() local={}, remote={}", - ctx.getInstanceId(), - channelContext.channel().localAddress(), - channelContext.channel().remoteAddress() - ); - } finally { - config.getLoggingContext().keySet().forEach(MDC::remove); - } - - if (ctx.currentState() == State.Connected) { - ctx.fireEvent(new Event.ChannelInactive()); - } - - super.channelInactive(channelContext); - } - - @Override - public void exceptionCaught(ChannelHandlerContext channelContext, Throwable cause) { - config.getLoggingContext().forEach(MDC::put); - try { - logger.debug( - "[{}] exceptionCaught() local={}, remote={}", - ctx.getInstanceId(), - channelContext.channel().localAddress(), - channelContext.channel().remoteAddress(), - cause - ); - } finally { - config.getLoggingContext().keySet().forEach(MDC::remove); - } - - if (ctx.currentState() == State.Connected) { - channelContext.close(); - } - } - - @Override - public void userEventTriggered(ChannelHandlerContext channelContext, Object evt) throws Exception { - if (evt instanceof IdleStateEvent) { - IdleState idleState = ((IdleStateEvent) evt).state(); - - if (idleState == IdleState.READER_IDLE) { - config.getLoggingContext().forEach(MDC::put); - try { - logger.debug( - "[{}] channel idle, maxIdleSeconds={}", - ctx.getInstanceId(), config.getMaxIdleSeconds() - ); - } finally { - config.getLoggingContext().keySet().forEach(MDC::remove); - } - - ctx.fireEvent(new Event.ChannelIdle()); - } - } - - super.userEventTriggered(channelContext, evt); - } - }); - - - ConnectFuture cf = KEY_CF.get(ctx); - config.getExecutor().execute(() -> cf.future.complete(channel)); - }); - - fb.onInternalTransition(State.Connected) - .via(Event.Connect.class) - .execute(ctx -> handleConnectEvent(ctx, config)); - - fb.onInternalTransition(State.Connected) - .via(Event.GetChannel.class) - .execute(ctx -> handleGetChannelEvent(ctx, config)); - - fb.onInternalTransition(State.Connected) - .via(Event.ChannelIdle.class) - .execute(ctx -> { - ConnectFuture cf = KEY_CF.get(ctx); - - cf.future.thenAcceptAsync(ch -> { - CompletableFuture keepAliveFuture = - config.getChannelActions().keepAlive(ctx, ch); - - keepAliveFuture.whenComplete((v, ex) -> { - if (ex != null) { - ctx.fireEvent(new Event.KeepAliveFailure(ex)); - } - }); - }, config.getExecutor()); - }); - - fb.onTransitionFrom(State.Connected) - .to(s -> s == State.Idle || s == State.ReconnectWait) - .via(Event.KeepAliveFailure.class) - .execute(ctx -> { - ConnectFuture cf = KEY_CF.get(ctx); - - cf.future.thenAccept(Channel::close); - }); + fb.when(State.Connecting) + .on(Event.ConnectSuccess.class) + .transitionTo(State.Connected); + + fb.onTransitionTo(State.Connecting) + .from(s -> s != State.Connecting) + .via(e -> e.getClass() == Event.Connect.class) + .execute(ctx -> { + ConnectFuture cf = new ConnectFuture(); + KEY_CF.set(ctx, cf); + + handleConnectEvent(ctx, config); + + connect(ctx, config); + }); + + fb.onInternalTransition(State.Connecting) + .via(Event.Connect.class) + .execute(ctx -> handleConnectEvent(ctx, config)); + + fb.onInternalTransition(State.Connecting) + .via(Event.GetChannel.class) + .execute(ctx -> handleGetChannelEvent(ctx, config)); + + fb.onInternalTransition(State.Connecting) + .via(Event.Disconnect.class) + .execute(ctx -> ctx.shelveEvent(ctx.event())); + + fb.onTransitionFrom(State.Connecting) + .to(s -> s != State.Connecting) + .viaAny() + .execute(FsmContext::processShelvedEvents); + + fb.onTransitionFrom(State.Connecting) + .to(s -> s != State.Connecting) + .via(Event.ConnectFailure.class) + .execute(ctx -> handleConnectFailureEvent(ctx, config)); + } + + private static void configureConnectedState( + FsmBuilder fb, + ChannelFsmConfig config + ) { + + fb.when(State.Connected) + .on(Event.Disconnect.class) + .transitionTo(State.Disconnecting); + + if (config.isLazy()) { + fb.when(State.Connected) + .on(e -> + e.getClass() == Event.ChannelInactive.class + || e.getClass() == Event.KeepAliveFailure.class) + .transitionTo(State.Idle); + } else { + fb.when(State.Connected) + .on(e -> + e.getClass() == Event.ChannelInactive.class + || e.getClass() == Event.KeepAliveFailure.class) + .transitionTo(State.ReconnectWait); } - private static void configureDisconnectingState(FsmBuilder fb, ChannelFsmConfig config) { - fb.when(State.Disconnecting) - .on(Event.DisconnectSuccess.class) - .transitionTo(State.NotConnected); - - fb.onTransitionTo(State.Disconnecting) - .from(State.Connected) - .via(Event.Disconnect.class) - .execute(ctx -> { - DisconnectFuture df = new DisconnectFuture(); - KEY_DF.set(ctx, df); - - Event.Disconnect event = (Event.Disconnect) ctx.event(); - - completeAsync(event.disconnectFuture, config.getExecutor()).with(df.future); - - disconnect(ctx, config); - }); + fb.onTransitionTo(State.Connected) + .from(s -> s != State.Connected) + .via(Event.ConnectSuccess.class) + .execute(ctx -> { + Event.ConnectSuccess event = (Event.ConnectSuccess) ctx.event(); + Channel channel = event.channel; + + if (config.getMaxIdleSeconds() > 0) { + channel.pipeline() + .addFirst(new IdleStateHandler(config.getMaxIdleSeconds(), 0, 0)); + } + + channel.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelInactive( + ChannelHandlerContext channelContext + ) throws Exception { + + Log.debug( + config.getContext(), + "channelInactive() local=%s, remote=%s", + channelContext.channel().localAddress(), + channelContext.channel().remoteAddress() + ); + + if (ctx.currentState() == State.Connected) { + ctx.fireEvent(new Event.ChannelInactive()); + } + + super.channelInactive(channelContext); + } - fb.onInternalTransition(State.Disconnecting) - .via(e -> e.getClass() == Event.Connect.class || e.getClass() == Event.GetChannel.class) - .execute(ctx -> ctx.shelveEvent(ctx.event())); + @Override + public void exceptionCaught( + ChannelHandlerContext channelContext, + Throwable cause + ) { + + Log.debug( + config.getContext(), + "exceptionCaught() local=%s, remote=%s%n%s", + channelContext.channel().localAddress(), + channelContext.channel().remoteAddress(), + cause + ); + + if (ctx.currentState() == State.Connected) { + channelContext.close(); + } + } - fb.onInternalTransition(State.Disconnecting) - .via(Event.Disconnect.class) - .execute(ctx -> { - DisconnectFuture df = KEY_DF.get(ctx); + @Override + public void userEventTriggered( + ChannelHandlerContext channelContext, + Object evt + ) throws Exception { - if (df != null) { - Event.Disconnect event = (Event.Disconnect) ctx.event(); + if (evt instanceof IdleStateEvent) { + IdleState idleState = ((IdleStateEvent) evt).state(); - completeAsync(event.disconnectFuture, config.getExecutor()).with(df.future); - } - }); + if (idleState == IdleState.READER_IDLE) { + Log.debug( + config.getContext(), + "channel idle, maxIdleSeconds=%s", + config.getMaxIdleSeconds() + ); - fb.onTransitionFrom(State.Disconnecting) - .to(s -> s != State.Disconnecting) - .via(Event.DisconnectSuccess.class) - .execute(ctx -> { - DisconnectFuture df = KEY_DF.remove(ctx); - - if (df != null) { - config.getExecutor().execute(() -> df.future.complete(null)); + ctx.fireEvent(new Event.ChannelIdle()); } - }); + } - fb.onTransitionFrom(State.Disconnecting) - .to(s -> s != State.Disconnecting) - .viaAny() - .execute(FsmContext::processShelvedEvents); - } - - private static void configureReconnectWaitState(FsmBuilder fb, ChannelFsmConfig config) { - fb.when(State.ReconnectWait) - .on(Event.ReconnectDelayElapsed.class) - .transitionTo(State.Reconnecting); - - fb.when(State.ReconnectWait) - .on(Event.Disconnect.class) - .transitionTo(State.NotConnected); - - // This needs to be defined before the action after it so the previous - // ConnectFuture can be notified before a new ConnectFuture is set. - fb.onTransitionTo(State.ReconnectWait) - .from(State.Reconnecting) - .via(Event.ConnectFailure.class) - .execute(ctx -> handleConnectFailureEvent(ctx, config)); - - fb.onTransitionTo(State.ReconnectWait) - .from(s -> s != State.ReconnectWait) - .viaAny() - .execute(ctx -> { - KEY_CF.set(ctx, new ConnectFuture()); - - Long delay = KEY_RD.get(ctx); - if (delay == null) { - delay = 1L; - } else { - delay = Math.min(getMaxReconnectDelay(config), delay << 1); - } - KEY_RD.set(ctx, delay); - - Cancellable reconnectDelayFuture = config.getScheduler().schedule( - () -> - ctx.fireEvent(new Event.ReconnectDelayElapsed()), - delay, - TimeUnit.SECONDS - ); - - KEY_RDF.set(ctx, reconnectDelayFuture); - }); - - fb.onInternalTransition(State.ReconnectWait) - .via(Event.Connect.class) - .execute(ctx -> handleConnectEvent(ctx, config)); - - fb.onInternalTransition(State.ReconnectWait) - .via(Event.GetChannel.class) - .execute(ctx -> { - Event.GetChannel event = (Event.GetChannel) ctx.event(); - - if (event.waitForReconnect) { - handleGetChannelEvent(ctx, config); - } else { - config.getExecutor().execute(() -> - event.channelFuture - .completeExceptionally(new Exception("not reconnected")) - ); - } + super.userEventTriggered(channelContext, evt); + } + }); + + ConnectFuture cf = KEY_CF.get(ctx); + if (cf != null) { + config.getExecutor().execute(() -> cf.future.complete(channel)); + } + }); + + fb.onInternalTransition(State.Connected) + .via(Event.Connect.class) + .execute(ctx -> handleConnectEvent(ctx, config)); + + fb.onInternalTransition(State.Connected) + .via(Event.GetChannel.class) + .execute(ctx -> handleGetChannelEvent(ctx, config)); + + fb.onInternalTransition(State.Connected) + .via(Event.ChannelIdle.class) + .execute(ctx -> { + ConnectFuture cf = KEY_CF.get(ctx); + + cf.future.thenAcceptAsync(ch -> { + CompletableFuture keepAliveFuture = + config.getChannelActions().keepAlive(ctx, ch); + + keepAliveFuture.whenComplete((v, ex) -> { + if (ex != null) { + ctx.fireEvent(new Event.KeepAliveFailure(ex)); + } }); + }, config.getExecutor()); + }); + + fb.onTransitionFrom(State.Connected) + .to(s -> s == State.Idle || s == State.ReconnectWait) + .via(Event.KeepAliveFailure.class) + .execute(ctx -> { + ConnectFuture cf = KEY_CF.get(ctx); + + cf.future.thenAccept(Channel::close); + }); + } + + private static void configureDisconnectingState( + FsmBuilder fb, + ChannelFsmConfig config + ) { + + fb.when(State.Disconnecting) + .on(Event.DisconnectSuccess.class) + .transitionTo(State.NotConnected); + + fb.onTransitionTo(State.Disconnecting) + .from(State.Connected) + .via(Event.Disconnect.class) + .execute(ctx -> { + DisconnectFuture df = new DisconnectFuture(); + KEY_DF.set(ctx, df); + + Event.Disconnect event = (Event.Disconnect) ctx.event(); + + completeAsync(event.disconnectFuture, config.getExecutor()).with(df.future); + + disconnect(ctx, config); + }); + + fb.onInternalTransition(State.Disconnecting) + .via(e -> e.getClass() == Event.Connect.class || e.getClass() == Event.GetChannel.class) + .execute(ctx -> ctx.shelveEvent(ctx.event())); + + fb.onInternalTransition(State.Disconnecting) + .via(Event.Disconnect.class) + .execute(ctx -> { + DisconnectFuture df = KEY_DF.get(ctx); + + if (df != null) { + Event.Disconnect event = (Event.Disconnect) ctx.event(); + + completeAsync(event.disconnectFuture, config.getExecutor()).with(df.future); + } + }); + + fb.onTransitionFrom(State.Disconnecting) + .to(s -> s != State.Disconnecting) + .via(Event.DisconnectSuccess.class) + .execute(ctx -> { + DisconnectFuture df = KEY_DF.remove(ctx); + + if (df != null) { + config.getExecutor().execute(() -> df.future.complete(null)); + } + }); + + fb.onTransitionFrom(State.Disconnecting) + .to(s -> s != State.Disconnecting) + .viaAny() + .execute(FsmContext::processShelvedEvents); + } + + private static void configureReconnectWaitState( + FsmBuilder fb, + ChannelFsmConfig config + ) { + + fb.when(State.ReconnectWait) + .on(Event.ReconnectDelayElapsed.class) + .transitionTo(State.Reconnecting); + + fb.when(State.ReconnectWait) + .on(Event.Disconnect.class) + .transitionTo(State.NotConnected); + + // This needs to be defined before the action after it so the previous + // ConnectFuture can be notified before a new ConnectFuture is set. + fb.onTransitionTo(State.ReconnectWait) + .from(State.Reconnecting) + .via(Event.ConnectFailure.class) + .execute(ctx -> handleConnectFailureEvent(ctx, config)); + + fb.onTransitionTo(State.ReconnectWait) + .from(s -> s != State.ReconnectWait) + .viaAny() + .execute(ctx -> { + KEY_CF.set(ctx, new ConnectFuture()); + + Long delay = KEY_RD.get(ctx); + if (delay == null) { + delay = 1L; + } else { + delay = Math.min(getMaxReconnectDelay(config), delay << 1); + } + KEY_RD.set(ctx, delay); + + Cancellable reconnectDelayFuture = config.getScheduler().schedule( + () -> + ctx.fireEvent(new Event.ReconnectDelayElapsed()), + delay, + TimeUnit.SECONDS + ); + + KEY_RDF.set(ctx, reconnectDelayFuture); + }); + + fb.onInternalTransition(State.ReconnectWait) + .via(Event.Connect.class) + .execute(ctx -> handleConnectEvent(ctx, config)); + + fb.onInternalTransition(State.ReconnectWait) + .via(Event.GetChannel.class) + .execute(ctx -> { + Event.GetChannel event = (Event.GetChannel) ctx.event(); + + if (event.waitForReconnect) { + handleGetChannelEvent(ctx, config); + } else { + config.getExecutor().execute(() -> + event.channelFuture + .completeExceptionally(new Exception("not reconnected")) + ); + } + }); + + fb.onTransitionFrom(State.ReconnectWait) + .to(State.NotConnected) + .via(Event.Disconnect.class) + .execute(ctx -> { + ConnectFuture connectFuture = KEY_CF.remove(ctx); + if (connectFuture != null) { + config.getExecutor().execute(() -> + connectFuture.future + .completeExceptionally(new Exception("client disconnected")) + ); + } + + KEY_RD.remove(ctx); + + Cancellable reconnectDelayCancellable = KEY_RDF.remove(ctx); + if (reconnectDelayCancellable != null) { + reconnectDelayCancellable.cancel(); + } + + Event.Disconnect disconnect = (Event.Disconnect) ctx.event(); + config.getExecutor().execute(() -> + disconnect.disconnectFuture.complete(null) + ); + }); + } + + private static void configureReconnectingState( + FsmBuilder fb, + ChannelFsmConfig config + ) { + + fb.when(State.Reconnecting) + .on(Event.ConnectFailure.class) + .transitionTo(State.ReconnectWait); + + fb.when(State.Reconnecting) + .on(Event.ConnectSuccess.class) + .transitionTo(State.Connected); + + fb.onTransitionTo(State.Reconnecting) + .from(State.ReconnectWait) + .via(Event.ReconnectDelayElapsed.class) + .execute(ctx -> connect(ctx, config)); + + fb.onTransitionTo(State.Reconnecting) + .from(State.Idle) + .via(e -> e.getClass() == Event.Connect.class || e.getClass() == Event.GetChannel.class) + .execute(ctx -> { + ConnectFuture cf = new ConnectFuture(); + KEY_CF.set(ctx, cf); + + Event event = ctx.event(); + + if (event instanceof Event.Connect) { + handleConnectEvent(ctx, config); + } else if (event instanceof Event.GetChannel) { + handleGetChannelEvent(ctx, config); + } + + connect(ctx, config); + }); + + fb.onInternalTransition(State.Reconnecting) + .via(Event.Connect.class) + .execute(ctx -> handleConnectEvent(ctx, config)); + + fb.onInternalTransition(State.Reconnecting) + .via(Event.GetChannel.class) + .execute(ctx -> handleGetChannelEvent(ctx, config)); + + fb.onInternalTransition(State.Reconnecting) + .via(Event.Disconnect.class) + .execute(ctx -> ctx.shelveEvent(ctx.event())); + + fb.onTransitionFrom(State.Reconnecting) + .to(s -> s != State.Reconnecting) + .viaAny() + .execute(FsmContext::processShelvedEvents); + + fb.onTransitionFrom(State.Reconnecting) + .to(State.Connected) + .via(Event.ConnectSuccess.class) + .execute(ctx -> { + KEY_RD.remove(ctx); + KEY_RDF.remove(ctx); + }); + } + + private static void connect( + ActionContext ctx, + ChannelFsmConfig config + ) { + + config.getExecutor().execute(() -> + config.getChannelActions().connect(ctx).whenComplete((channel, ex) -> { + if (channel != null) { + ctx.fireEvent(new Event.ConnectSuccess(channel)); + } else { + ctx.fireEvent(new Event.ConnectFailure(ex)); + } + }) + ); + } + + private static void disconnect( + ActionContext ctx, + ChannelFsmConfig config + ) { + + ConnectFuture connectFuture = KEY_CF.get(ctx); + + if (connectFuture != null && connectFuture.future.isDone()) { + config.getExecutor().execute(() -> { + CompletableFuture disconnectFuture = config.getChannelActions().disconnect( + ctx, + connectFuture.future.getNow(null) + ); - fb.onTransitionFrom(State.ReconnectWait) - .to(State.NotConnected) - .via(Event.Disconnect.class) - .execute(ctx -> { - ConnectFuture connectFuture = KEY_CF.remove(ctx); - if (connectFuture != null) { - config.getExecutor().execute(() -> - connectFuture.future - .completeExceptionally(new Exception("client disconnected")) - ); - } - - KEY_RD.remove(ctx); - - Cancellable reconnectDelayCancellable = KEY_RDF.remove(ctx); - if (reconnectDelayCancellable != null) { - reconnectDelayCancellable.cancel(); - } - - Event.Disconnect disconnect = (Event.Disconnect) ctx.event(); - config.getExecutor().execute(() -> - disconnect.disconnectFuture.complete(null) - ); - }); + disconnectFuture.whenComplete( + (v, ex) -> ctx.fireEvent(new Event.DisconnectSuccess())); + }); + } else { + ctx.fireEvent(new Event.DisconnectSuccess()); } + } - private static void configureReconnectingState(FsmBuilder fb, ChannelFsmConfig config) { - fb.when(State.Reconnecting) - .on(Event.ConnectFailure.class) - .transitionTo(State.ReconnectWait); + private static void handleConnectEvent( + ActionContext ctx, + ChannelFsmConfig config + ) { - fb.when(State.Reconnecting) - .on(Event.ConnectSuccess.class) - .transitionTo(State.Connected); + CompletableFuture channelFuture = KEY_CF.get(ctx).future; - fb.onTransitionTo(State.Reconnecting) - .from(State.ReconnectWait) - .via(Event.ReconnectDelayElapsed.class) - .execute(ctx -> connect(ctx, config)); + Event.Connect connectEvent = (Event.Connect) ctx.event(); + completeAsync(connectEvent.channelFuture, config.getExecutor()).with(channelFuture); + } - fb.onTransitionTo(State.Reconnecting) - .from(State.Idle) - .via(e -> e.getClass() == Event.Connect.class || e.getClass() == Event.GetChannel.class) - .execute(ctx -> { - ConnectFuture cf = new ConnectFuture(); - KEY_CF.set(ctx, cf); + private static void handleGetChannelEvent( + ActionContext ctx, + ChannelFsmConfig config + ) { - Event event = ctx.event(); + CompletableFuture channelFuture = KEY_CF.get(ctx).future; - if (event instanceof Event.Connect) { - handleConnectEvent(ctx, config); - } else if (event instanceof Event.GetChannel) { - handleGetChannelEvent(ctx, config); - } + Event.GetChannel getChannelEvent = (Event.GetChannel) ctx.event(); + completeAsync(getChannelEvent.channelFuture, config.getExecutor()).with(channelFuture); + } - connect(ctx, config); - }); - - fb.onInternalTransition(State.Reconnecting) - .via(Event.Connect.class) - .execute(ctx -> handleConnectEvent(ctx, config)); - - fb.onInternalTransition(State.Reconnecting) - .via(Event.GetChannel.class) - .execute(ctx -> handleGetChannelEvent(ctx, config)); - - fb.onInternalTransition(State.Reconnecting) - .via(Event.Disconnect.class) - .execute(ctx -> ctx.shelveEvent(ctx.event())); - - fb.onTransitionFrom(State.Reconnecting) - .to(s -> s != State.Reconnecting) - .viaAny() - .execute(FsmContext::processShelvedEvents); - - fb.onTransitionFrom(State.Reconnecting) - .to(State.Connected) - .via(Event.ConnectSuccess.class) - .execute(ctx -> { - KEY_RD.remove(ctx); - KEY_RDF.remove(ctx); - }); - } + private static void handleConnectFailureEvent( + ActionContext ctx, + ChannelFsmConfig config + ) { - private static void connect( - ActionContext ctx, - ChannelFsmConfig config - ) { - - config.getExecutor().execute(() -> - config.getChannelActions().connect(ctx).whenComplete((channel, ex) -> { - if (channel != null) { - ctx.fireEvent(new Event.ConnectSuccess(channel)); - } else { - ctx.fireEvent(new Event.ConnectFailure(ex)); - } - }) - ); - } + ConnectFuture cf = KEY_CF.remove(ctx); - private static void disconnect( - ActionContext ctx, - ChannelFsmConfig config - ) { + if (cf != null) { + Event.ConnectFailure connectFailureEvent = (Event.ConnectFailure) ctx.event(); - ConnectFuture connectFuture = KEY_CF.get(ctx); - - if (connectFuture != null && connectFuture.future.isDone()) { - config.getExecutor().execute(() -> { - CompletableFuture disconnectFuture = config.getChannelActions().disconnect( - ctx, - connectFuture.future.getNow(null) - ); - - disconnectFuture.whenComplete((v, ex) -> ctx.fireEvent(new Event.DisconnectSuccess())); - }); - } else { - ctx.fireEvent(new Event.DisconnectSuccess()); - } - } - - private static void handleConnectEvent(ActionContext ctx, ChannelFsmConfig config) { - CompletableFuture channelFuture = KEY_CF.get(ctx).future; - - Event.Connect connectEvent = (Event.Connect) ctx.event(); - completeAsync(connectEvent.channelFuture, config.getExecutor()).with(channelFuture); - } - - private static void handleGetChannelEvent(ActionContext ctx, ChannelFsmConfig config) { - CompletableFuture channelFuture = KEY_CF.get(ctx).future; - - Event.GetChannel getChannelEvent = (Event.GetChannel) ctx.event(); - completeAsync(getChannelEvent.channelFuture, config.getExecutor()).with(channelFuture); + config.getExecutor().execute(() -> + cf.future.completeExceptionally(connectFailureEvent.failure) + ); } + } - private static void handleConnectFailureEvent(ActionContext ctx, ChannelFsmConfig config) { - ConnectFuture cf = KEY_CF.remove(ctx); + private static int getMaxReconnectDelay(ChannelFsmConfig config) { + int maxReconnectDelay = config.getMaxReconnectDelaySeconds(); - if (cf != null) { - Event.ConnectFailure connectFailureEvent = (Event.ConnectFailure) ctx.event(); - - config.getExecutor().execute(() -> - cf.future.completeExceptionally(connectFailureEvent.failure) - ); - } + if (maxReconnectDelay < 1) { + maxReconnectDelay = ChannelFsmConfigBuilder.DEFAULT_MAX_RECONNECT_DELAY_SECONDS; } - private static int getMaxReconnectDelay(ChannelFsmConfig config) { - int maxReconnectDelay = config.getMaxReconnectDelaySeconds(); - - if (maxReconnectDelay < 1) { - maxReconnectDelay = ChannelFsmConfigBuilder.DEFAULT_MAX_RECONNECT_DELAY_SECONDS; - } - - int highestOneBit = Integer.highestOneBit(maxReconnectDelay); + int highestOneBit = Integer.highestOneBit(maxReconnectDelay); - if (maxReconnectDelay == highestOneBit) { - return maxReconnectDelay; - } else { - return highestOneBit << 1; - } + if (maxReconnectDelay == highestOneBit) { + return maxReconnectDelay; + } else { + return highestOneBit << 1; } + } } diff --git a/src/main/java/com/digitalpetri/netty/fsm/CompletionBuilders.java b/src/main/java/com/digitalpetri/netty/fsm/CompletionBuilders.java new file mode 100644 index 0000000..48d61ba --- /dev/null +++ b/src/main/java/com/digitalpetri/netty/fsm/CompletionBuilders.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 Kevin Herron + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package com.digitalpetri.netty.fsm; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +class CompletionBuilders { + + private CompletionBuilders() {} + + /** + * Complete {@code future} with the result of the {@link CompletableFuture} that is provided to + * the returned {@link CompletionBuilder}. + * + * @param future the future to complete. + * @param the type returned by {@code future}. + * @return a {@link CompletionBuilder}. + */ + public static CompletionBuilder complete(CompletableFuture future) { + return new CompletionBuilder<>(future); + } + + /** + * Complete {@code future} asynchronously with the result of the {@link CompletableFuture} that is + * provided to the returned {@link CompletionBuilder}. + * + * @param future the future to complete. + * @param executor the {@link Executor} to use. + * @param the type returned by {@code future}. + * @return a {@link CompletionBuilder}. + */ + public static CompletionBuilder completeAsync(CompletableFuture future, + Executor executor) { + return new AsyncCompletionBuilder<>(future, executor); + } + + public static class CompletionBuilder { + + final CompletableFuture toComplete; + + private CompletionBuilder(CompletableFuture toComplete) { + this.toComplete = toComplete; + } + + /** + * Turn this {@link CompletionBuilder} into an {@link AsyncCompletionBuilder}. + * + * @param executor the {@link Executor} to use for the async completion. + * @return an {@link AsyncCompletionBuilder}. + */ + public CompletionBuilder async(Executor executor) { + return new AsyncCompletionBuilder<>(toComplete, executor); + } + + /** + * Complete the contained to-be-completed {@link CompletableFuture} using the result of + * {@code future}. + * + * @param future the {@link CompletableFuture} to use as the result for the contained + * future. + * @return the original, to-be-completed future provided to this {@link CompletionBuilder}. + */ + public CompletableFuture with(CompletableFuture future) { + future.whenComplete((v, ex) -> { + if (ex != null) { + toComplete.completeExceptionally(ex); + } else { + toComplete.complete(v); + } + }); + + return toComplete; + } + + } + + private static final class AsyncCompletionBuilder extends CompletionBuilder { + + private final Executor executor; + + AsyncCompletionBuilder(CompletableFuture toComplete, Executor executor) { + super(toComplete); + + this.executor = executor; + } + + @Override + public CompletableFuture with(CompletableFuture future) { + future.whenCompleteAsync((v, ex) -> { + if (ex != null) { + toComplete.completeExceptionally(ex); + } else { + toComplete.complete(v); + } + }, executor); + + return toComplete; + } + + } + +} diff --git a/src/main/java/com/digitalpetri/netty/fsm/Event.java b/src/main/java/com/digitalpetri/netty/fsm/Event.java index 1da0e36..2511cc0 100644 --- a/src/main/java/com/digitalpetri/netty/fsm/Event.java +++ b/src/main/java/com/digitalpetri/netty/fsm/Event.java @@ -1,129 +1,132 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm; -import java.util.concurrent.CompletableFuture; - import io.netty.channel.Channel; +import java.util.concurrent.CompletableFuture; public interface Event { - class ChannelIdle implements Event { - @Override - public String toString() { - return getClass().getSimpleName(); - } + class ChannelIdle implements Event { + + @Override + public String toString() { + return getClass().getSimpleName(); } + } - class ChannelInactive implements Event { - @Override - public String toString() { - return getClass().getSimpleName(); - } + class ChannelInactive implements Event { + + @Override + public String toString() { + return getClass().getSimpleName(); } + } + + class Connect implements Event { - class Connect implements Event { - final CompletableFuture channelFuture = new CompletableFuture<>(); + final CompletableFuture channelFuture = new CompletableFuture<>(); - @Override - public String toString() { - return getClass().getSimpleName(); - } + @Override + public String toString() { + return getClass().getSimpleName(); } + } - class ConnectSuccess implements Event { - final Channel channel; + class ConnectSuccess implements Event { - public ConnectSuccess(Channel channel) { - this.channel = channel; - } + final Channel channel; - @Override - public String toString() { - return getClass().getSimpleName(); - } + public ConnectSuccess(Channel channel) { + this.channel = channel; } - class ConnectFailure implements Event { - final Throwable failure; + @Override + public String toString() { + return getClass().getSimpleName(); + } + } - public ConnectFailure(Throwable failure) { - this.failure = failure; - } + class ConnectFailure implements Event { - @Override - public String toString() { - return getClass().getSimpleName(); - } + final Throwable failure; + + public ConnectFailure(Throwable failure) { + this.failure = failure; } - class Disconnect implements Event { - final CompletableFuture disconnectFuture = new CompletableFuture<>(); + @Override + public String toString() { + return getClass().getSimpleName(); + } + } - @Override - public String toString() { - return getClass().getSimpleName(); - } + class Disconnect implements Event { + + final CompletableFuture disconnectFuture = new CompletableFuture<>(); + + @Override + public String toString() { + return getClass().getSimpleName(); } + } + + class DisconnectSuccess implements Event { - class DisconnectSuccess implements Event { - @Override - public String toString() { - return getClass().getSimpleName(); - } + @Override + public String toString() { + return getClass().getSimpleName(); } + } - class GetChannel implements Event { - final CompletableFuture channelFuture = new CompletableFuture<>(); + class GetChannel implements Event { - final boolean waitForReconnect; + final CompletableFuture channelFuture = new CompletableFuture<>(); - GetChannel() { - this(true); - } + final boolean waitForReconnect; - GetChannel(boolean waitForReconnect) { - this.waitForReconnect = waitForReconnect; - } + GetChannel() { + this(true); + } - @Override - public String toString() { - return getClass().getSimpleName(); - } + GetChannel(boolean waitForReconnect) { + this.waitForReconnect = waitForReconnect; } - class KeepAliveFailure implements Event { - final Throwable failure; + @Override + public String toString() { + return getClass().getSimpleName(); + } + } - KeepAliveFailure(Throwable failure) { - this.failure = failure; - } + class KeepAliveFailure implements Event { - @Override - public String toString() { - return getClass().getSimpleName(); - } + final Throwable failure; + + KeepAliveFailure(Throwable failure) { + this.failure = failure; + } + + @Override + public String toString() { + return getClass().getSimpleName(); } + } + + class ReconnectDelayElapsed implements Event { - class ReconnectDelayElapsed implements Event { - @Override - public String toString() { - return getClass().getSimpleName(); - } + @Override + public String toString() { + return getClass().getSimpleName(); } + } } diff --git a/src/main/java/com/digitalpetri/netty/fsm/Scheduler.java b/src/main/java/com/digitalpetri/netty/fsm/Scheduler.java index 662618b..9dfed21 100644 --- a/src/main/java/com/digitalpetri/netty/fsm/Scheduler.java +++ b/src/main/java/com/digitalpetri/netty/fsm/Scheduler.java @@ -1,17 +1,11 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm; @@ -22,39 +16,39 @@ public interface Scheduler { - /** - * Schedule a command to run after a {@code delay}. - * - * @param command the commadn to run. - * @param delay the time to delay. - * @param unit the time unit of the delay. - * @return a {@link Cancellable} that can be used to attempt cancellation if needed. - */ - Cancellable schedule(Runnable command, long delay, TimeUnit unit); + /** + * Schedule a command to run after a {@code delay}. + * + * @param command the commadn to run. + * @param delay the time to delay. + * @param unit the time unit of the delay. + * @return a {@link Cancellable} that can be used to attempt cancellation if needed. + */ + Cancellable schedule(Runnable command, long delay, TimeUnit unit); - interface Cancellable { - - /** - * Attempt to cancel a scheduled command. - * - * @return {@code true} if the command was canceled. - */ - boolean cancel(); - - } + interface Cancellable { /** - * Create a {@link Scheduler} from the provided {@link ScheduledExecutorService}. + * Attempt to cancel a scheduled command. * - * @param scheduledExecutor a {@link ScheduledExecutorService}. - * @return a {@link Scheduler}. + * @return {@code true} if the command was canceled. */ - static Scheduler fromScheduledExecutor(ScheduledExecutorService scheduledExecutor) { - return (command, delay, unit) -> { - ScheduledFuture future = scheduledExecutor.schedule(command, delay, unit); - - return () -> future.cancel(false); - }; - } + boolean cancel(); + + } + + /** + * Create a {@link Scheduler} from the provided {@link ScheduledExecutorService}. + * + * @param scheduledExecutor a {@link ScheduledExecutorService}. + * @return a {@link Scheduler}. + */ + static Scheduler fromScheduledExecutor(ScheduledExecutorService scheduledExecutor) { + return (command, delay, unit) -> { + ScheduledFuture future = scheduledExecutor.schedule(command, delay, unit); + + return () -> future.cancel(false); + }; + } } diff --git a/src/main/java/com/digitalpetri/netty/fsm/State.java b/src/main/java/com/digitalpetri/netty/fsm/State.java index b1a65a4..9008320 100644 --- a/src/main/java/com/digitalpetri/netty/fsm/State.java +++ b/src/main/java/com/digitalpetri/netty/fsm/State.java @@ -1,27 +1,21 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm; public enum State { - Connecting, - Connected, - Disconnecting, - Idle, - NotConnected, - ReconnectWait, - Reconnecting + Connecting, + Connected, + Disconnecting, + Idle, + NotConnected, + ReconnectWait, + Reconnecting } diff --git a/src/main/java/com/digitalpetri/netty/fsm/util/CompletionBuilders.java b/src/main/java/com/digitalpetri/netty/fsm/util/CompletionBuilders.java deleted file mode 100644 index ef3d880..0000000 --- a/src/main/java/com/digitalpetri/netty/fsm/util/CompletionBuilders.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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. - */ - -package com.digitalpetri.netty.fsm.util; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; - -public class CompletionBuilders { - - /** - * Complete {@code future} with the result of the {@link CompletableFuture} that is provided to the returned - * {@link CompletionBuilder}. - * - * @param future the future to complete. - * @param the type returned by {@code future}. - * @return a {@link CompletionBuilder}. - */ - public static CompletionBuilder complete(CompletableFuture future) { - return new CompletionBuilder<>(future); - } - - /** - * Complete {@code future} asynchronously with the result of the {@link CompletableFuture} that is provided to - * the returned {@link CompletionBuilder}. - * - * @param future the future to complete. - * @param executor the {@link Executor} to use. - * @param the type returned by {@code future}. - * @return a {@link CompletionBuilder}. - */ - public static CompletionBuilder completeAsync(CompletableFuture future, Executor executor) { - return new AsyncCompletionBuilder<>(future, executor); - } - - public static class CompletionBuilder { - - final CompletableFuture toComplete; - - private CompletionBuilder(CompletableFuture toComplete) { - this.toComplete = toComplete; - } - - /** - * Turn this {@link CompletionBuilder} into an {@link AsyncCompletionBuilder}. - * - * @param executor the {@link Executor} to use for the async completion. - * @return an {@link AsyncCompletionBuilder}. - */ - public CompletionBuilder async(Executor executor) { - return new AsyncCompletionBuilder<>(toComplete, executor); - } - - /** - * Complete the contained to-be-completed {@link CompletableFuture} using the result of {@code future}. - * - * @param future the {@link CompletableFuture} to use as the result for the contained future. - * @return the original, to-be-completed future provided to this {@link CompletionBuilder}. - */ - public CompletableFuture with(CompletableFuture future) { - future.whenComplete((v, ex) -> { - if (ex != null) toComplete.completeExceptionally(ex); - else toComplete.complete(v); - }); - - return toComplete; - } - - } - - private static final class AsyncCompletionBuilder extends CompletionBuilder { - - private final Executor executor; - - AsyncCompletionBuilder(CompletableFuture toComplete, Executor executor) { - super(toComplete); - - this.executor = executor; - } - - @Override - public CompletableFuture with(CompletableFuture future) { - future.whenCompleteAsync((v, ex) -> { - if (ex != null) toComplete.completeExceptionally(ex); - else toComplete.complete(v); - }, executor); - - return toComplete; - } - - } - -} diff --git a/src/test/java/com/digitalpetri/netty/fsm/DisconnectingTransitions.kt b/src/test/java/com/digitalpetri/netty/fsm/DisconnectingTransitions.kt deleted file mode 100644 index 831b8ae..0000000 --- a/src/test/java/com/digitalpetri/netty/fsm/DisconnectingTransitions.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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. - */ - -package com.digitalpetri.netty.fsm - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - - -class DisconnectingTransitions { - - @Test - fun `S(DISCONNECTING) x E(DisconnectSuccess) = S'(NOT_CONNECTED)`() { - val fsm = factory().newChannelFsm(State.Disconnecting) - val event = Event.DisconnectSuccess() - - assertEquals(State.NotConnected, fsm.fsm.fireEventBlocking(event)) - } - -} diff --git a/src/test/java/com/digitalpetri/netty/fsm/IdleActions.kt b/src/test/java/com/digitalpetri/netty/fsm/IdleActions.kt deleted file mode 100644 index f8c6689..0000000 --- a/src/test/java/com/digitalpetri/netty/fsm/IdleActions.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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. - */ - -package com.digitalpetri.netty.fsm - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - - -class IdleActions { - - @Test - fun `Transition from IDLE to NOT_CONNECTED via Disconnect completes Disconnect future`() { - val fsm = factory().newChannelFsm(State.Idle) - - val disconnect = Event.Disconnect() - assertEquals(State.NotConnected, fsm.fsm.fireEventBlocking(disconnect)) - - assertWithTimeout { - disconnect.disconnectFuture.get() - } - - } - -} diff --git a/src/test/java/com/digitalpetri/netty/fsm/NotConnectedTransitions.kt b/src/test/java/com/digitalpetri/netty/fsm/NotConnectedTransitions.kt deleted file mode 100644 index 69a8f9e..0000000 --- a/src/test/java/com/digitalpetri/netty/fsm/NotConnectedTransitions.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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. - */ - -package com.digitalpetri.netty.fsm - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - - -class NotConnectedTransitions { - - @Test - fun `S(NOT_CONNECTED) x E(Connect) = S'(CONNECTING)`() { - val fsm = factory().newChannelFsm() - - val event = Event.Connect() - - assertEquals(State.Connecting, fsm.fsm.fireEventBlocking(event)) { - "expected State.CONNECTING" - } - } - -} - - - diff --git a/src/test/java/com/digitalpetri/netty/fsm/ConnectedActions.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/ConnectedActionsTest.kt similarity index 85% rename from src/test/java/com/digitalpetri/netty/fsm/ConnectedActions.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/ConnectedActionsTest.kt index 4b21de4..992344c 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/ConnectedActions.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/ConnectedActionsTest.kt @@ -1,22 +1,16 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm -import com.digitalpetri.strictmachine.FsmContext +import com.digitalpetri.fsm.FsmContext import io.netty.channel.Channel import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test @@ -25,7 +19,7 @@ import java.util.concurrent.CompletableFuture.completedFuture import java.util.concurrent.CountDownLatch -class ConnectedActions { +class ConnectedActionsTest { @Test fun `External transition to CONNECTED via ConnectSuccess`() { diff --git a/src/test/java/com/digitalpetri/netty/fsm/ConnectedTransitions.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/ConnectedTransitionsTest.kt similarity index 71% rename from src/test/java/com/digitalpetri/netty/fsm/ConnectedTransitions.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/ConnectedTransitionsTest.kt index 0e47ff7..0eef8b1 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/ConnectedTransitions.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/ConnectedTransitionsTest.kt @@ -1,17 +1,11 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm @@ -20,7 +14,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -class ConnectedTransitions { +class ConnectedTransitionsTest { @Test fun `S(CONNECTED) x E(Disconnect) = S'(DISCONNECTING)`() { diff --git a/src/test/java/com/digitalpetri/netty/fsm/ConnectingActions.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/ConnectingActionsTest.kt similarity index 86% rename from src/test/java/com/digitalpetri/netty/fsm/ConnectingActions.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/ConnectingActionsTest.kt index 08b8fb0..0fcbbad 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/ConnectingActions.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/ConnectingActionsTest.kt @@ -1,23 +1,17 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm +import com.digitalpetri.fsm.FsmContext import com.digitalpetri.netty.fsm.ChannelFsm.KEY_CF -import com.digitalpetri.strictmachine.FsmContext import io.netty.channel.Channel import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test @@ -26,7 +20,7 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.ExecutionException -class ConnectingActions { +class ConnectingActionsTest { @Test fun `External transition to CONNECTING via Connect`() { diff --git a/src/test/java/com/digitalpetri/netty/fsm/ConnectingTransitions.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/ConnectingTransitionsTest.kt similarity index 70% rename from src/test/java/com/digitalpetri/netty/fsm/ConnectingTransitions.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/ConnectingTransitionsTest.kt index 8effb13..1ff1f35 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/ConnectingTransitions.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/ConnectingTransitionsTest.kt @@ -1,17 +1,11 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm @@ -21,7 +15,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -class ConnectingTransitions { +class ConnectingTransitionsTest { @Test fun `S(CONNECTING) x E(ConnectFailure) = S'(IDLE) (persistent=true, lazy=true)`() { diff --git a/src/test/java/com/digitalpetri/netty/fsm/DisconnectingActions.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/DisconnectingActionsTest.kt similarity index 88% rename from src/test/java/com/digitalpetri/netty/fsm/DisconnectingActions.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/DisconnectingActionsTest.kt index e3acb76..d1c84d5 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/DisconnectingActions.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/DisconnectingActionsTest.kt @@ -1,17 +1,11 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm @@ -23,7 +17,7 @@ import org.junit.jupiter.api.assertThrows import java.util.concurrent.ExecutionException -class DisconnectingActions { +class DisconnectingActionsTest { @Test fun `External transition to DISCONNECTING from CONNECTED via Disconnect`() { diff --git a/src/test/kotlin/com/digitalpetri/netty/fsm/DisconnectingTransitionsTest.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/DisconnectingTransitionsTest.kt new file mode 100644 index 0000000..47de7be --- /dev/null +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/DisconnectingTransitionsTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 Kevin Herron + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package com.digitalpetri.netty.fsm + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + + +class DisconnectingTransitionsTest { + + @Test + fun `S(DISCONNECTING) x E(DisconnectSuccess) = S'(NOT_CONNECTED)`() { + val fsm = factory().newChannelFsm(State.Disconnecting) + val event = Event.DisconnectSuccess() + + assertEquals(State.NotConnected, fsm.fsm.fireEventBlocking(event)) + } + +} diff --git a/src/test/kotlin/com/digitalpetri/netty/fsm/IdleActionsTest.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/IdleActionsTest.kt new file mode 100644 index 0000000..013d71c --- /dev/null +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/IdleActionsTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Kevin Herron + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package com.digitalpetri.netty.fsm + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + + +class IdleActionsTest { + + @Test + fun `Transition from IDLE to NOT_CONNECTED via Disconnect completes Disconnect future`() { + val fsm = factory().newChannelFsm(State.Idle) + + val disconnect = Event.Disconnect() + assertEquals(State.NotConnected, fsm.fsm.fireEventBlocking(disconnect)) + + assertWithTimeout { + disconnect.disconnectFuture.get() + } + } + +} diff --git a/src/test/java/com/digitalpetri/netty/fsm/IdleTransitions.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/IdleTransitionsTest.kt similarity index 62% rename from src/test/java/com/digitalpetri/netty/fsm/IdleTransitions.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/IdleTransitionsTest.kt index 24a4b12..d34f56a 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/IdleTransitions.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/IdleTransitionsTest.kt @@ -1,17 +1,11 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm @@ -20,7 +14,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -class IdleTransitions { +class IdleTransitionsTest { @Test fun `S(IDLE) x E(Connect) = S'(RECONNECTING)`() { diff --git a/src/test/java/com/digitalpetri/netty/fsm/NotConnectedActions.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/NotConnectedActionsTest.kt similarity index 56% rename from src/test/java/com/digitalpetri/netty/fsm/NotConnectedActions.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/NotConnectedActionsTest.kt index 8a32efa..3fb15e4 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/NotConnectedActions.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/NotConnectedActionsTest.kt @@ -1,29 +1,22 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.util.concurrent.ExecutionException -class NotConnectedActions { +class NotConnectedActionsTest { @Test fun `Internal transition via Disconnect completes successfully`() { diff --git a/src/test/kotlin/com/digitalpetri/netty/fsm/NotConnectedTransitionsTest.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/NotConnectedTransitionsTest.kt new file mode 100644 index 0000000..4d75850 --- /dev/null +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/NotConnectedTransitionsTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 Kevin Herron + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package com.digitalpetri.netty.fsm + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + + +class NotConnectedTransitionsTest { + + @Test + fun `S(NOT_CONNECTED) x E(Connect) = S'(CONNECTING)`() { + val fsm = factory().newChannelFsm() + + val event = Event.Connect() + + assertEquals(State.Connecting, fsm.fsm.fireEventBlocking(event)) { + "expected State.CONNECTING" + } + } + +} + + + diff --git a/src/test/java/com/digitalpetri/netty/fsm/ReconnectWaitActions.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectWaitActionsTest.kt similarity index 93% rename from src/test/java/com/digitalpetri/netty/fsm/ReconnectWaitActions.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectWaitActionsTest.kt index 6c5beb3..ec78806 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/ReconnectWaitActions.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectWaitActionsTest.kt @@ -1,17 +1,11 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm @@ -23,7 +17,7 @@ import org.junit.jupiter.api.assertThrows import java.util.concurrent.ExecutionException -class ReconnectWaitActions { +class ReconnectWaitActionsTest { @Test fun `External transition to RECONNECT_WAIT from RECONNECTING via ConnectFailure notifies ConnectFuture`() { diff --git a/src/test/java/com/digitalpetri/netty/fsm/ReconnectWaitTransitions.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectWaitTransitionsTest.kt similarity index 52% rename from src/test/java/com/digitalpetri/netty/fsm/ReconnectWaitTransitions.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectWaitTransitionsTest.kt index e0fe103..b5f1c2e 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/ReconnectWaitTransitions.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectWaitTransitionsTest.kt @@ -1,17 +1,11 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm @@ -20,7 +14,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -class ReconnectWaitTransitions { +class ReconnectWaitTransitionsTest { @Test fun `S(RECONNECT_WAIT) x E(ReconnectDelayElapsed) = S'(RECONNECTING)`() { diff --git a/src/test/java/com/digitalpetri/netty/fsm/ReconnectingActions.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectingActionsTest.kt similarity index 87% rename from src/test/java/com/digitalpetri/netty/fsm/ReconnectingActions.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectingActionsTest.kt index 01e5f83..3a130e5 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/ReconnectingActions.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectingActionsTest.kt @@ -1,17 +1,11 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm @@ -22,7 +16,7 @@ import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test -class ReconnectingActions { +class ReconnectingActionsTest { @Test fun `Transition from RECONNECT_WAIT triggers connect()`() { diff --git a/src/test/java/com/digitalpetri/netty/fsm/ReconnectingTransitions.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectingTransitionsTest.kt similarity index 55% rename from src/test/java/com/digitalpetri/netty/fsm/ReconnectingTransitions.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectingTransitionsTest.kt index 77acc17..67fb02b 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/ReconnectingTransitions.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/ReconnectingTransitionsTest.kt @@ -1,17 +1,11 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm @@ -21,7 +15,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -class ReconnectingTransitions { +class ReconnectingTransitionsTest { @Test fun `S(RECONNECTING) x E(ConnectFailure) = S'(RECONNECT_WAIT)`() { diff --git a/src/test/java/com/digitalpetri/netty/fsm/TransitionListeners.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/TransitionListenersTest.kt similarity index 75% rename from src/test/java/com/digitalpetri/netty/fsm/TransitionListeners.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/TransitionListenersTest.kt index 7967b12..299156c 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/TransitionListeners.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/TransitionListenersTest.kt @@ -1,3 +1,13 @@ +/* + * Copyright (c) 2024 Kevin Herron + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + package com.digitalpetri.netty.fsm import org.junit.jupiter.api.Assertions.assertEquals @@ -5,7 +15,7 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.util.concurrent.atomic.AtomicBoolean -class TransitionListeners { +class TransitionListenersTest { @Test fun `TransitionListeners receive callbacks`() { diff --git a/src/test/java/com/digitalpetri/netty/fsm/fsm.kt b/src/test/kotlin/com/digitalpetri/netty/fsm/fsm.kt similarity index 91% rename from src/test/java/com/digitalpetri/netty/fsm/fsm.kt rename to src/test/kotlin/com/digitalpetri/netty/fsm/fsm.kt index b8f7441..3b2c577 100644 --- a/src/test/java/com/digitalpetri/netty/fsm/fsm.kt +++ b/src/test/kotlin/com/digitalpetri/netty/fsm/fsm.kt @@ -1,22 +1,16 @@ /* - * Copyright 2018 Kevin Herron + * Copyright (c) 2024 Kevin Herron * - * 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 + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * 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. + * SPDX-License-Identifier: EPL-2.0 */ package com.digitalpetri.netty.fsm -import com.digitalpetri.strictmachine.FsmContext +import com.digitalpetri.fsm.FsmContext import io.netty.channel.Channel import io.netty.channel.embedded.EmbeddedChannel import org.junit.jupiter.api.Assertions.assertTimeoutPreemptively