diff --git a/LICENSE b/LICENSE index e6b4097b..c9856029 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ - Network - High-performance, event-driven/reactive network stack for Java 11+ [The Apache Software License, Version 2.0] https://git.dorkbox.com/dorkbox/Network - Copyright 2022 + Copyright 2023 Dorkbox LLC Extra license information @@ -40,19 +40,41 @@ - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support [The Apache Software License, Version 2.0] https://github.com/Kotlin/kotlinx.coroutines - Copyright 2022 + Copyright 2023 JetBrains s.r.o. + - Javassist - Javassist (JAVA programming ASSISTant) makes Java bytecode manipulation simple + [The Apache Software License, Version 2.0] + https://www.javassist.org + https://github.com/jboss-javassist/javassist + Copyright 2023 + Shigeru Chiba + Bill Burke + Jason T. Greene + Licensed under the MPL/LGPL/Apache triple license + + - JNA - Simplified native library access for Java. + [The Apache Software License, Version 2.0] + https://github.com/twall/jna + Copyright 2023 + Timothy Wall + + - JNA-Platform - Mappings for a number of commonly used platform functions + [The Apache Software License, Version 2.0] + https://github.com/twall/jna + Copyright 2023 + Timothy Wall + - Aeron - Efficient reliable UDP unicast, UDP multicast, and IPC message transport [The Apache Software License, Version 2.0] https://github.com/real-logic/aeron - Copyright 2022 + Copyright 2023 Real Logic Limited - Kryo - Fast and efficient binary object graph serialization framework for Java [BSD 3-Clause License] https://github.com/EsotericSoftware/kryo - Copyright 2022 + Copyright 2023 Nathan Sweet Extra license information @@ -61,9 +83,9 @@ https://github.com/EsotericSoftware/reflectasm Nathan Sweet - - Objenesis - + - Objenesis - [The Apache Software License, Version 2.0] - http://objenesis.org + https://github.com/easymock/objenesis Objenesis Team and all contributors - MinLog-SLF4J - @@ -71,30 +93,19 @@ https://github.com/EsotericSoftware/minlog Nathan Sweet - - TypeTools - A simple, zero-dependency library for working with types. Supports Java 1.6+ and Android. + - LZ4 and xxHash - LZ4 compression for Java, based on Yann Collet's work [The Apache Software License, Version 2.0] - https://github.com/jhalterman/typetools - Copyright 2022 - Jonathan Halterman and friends + https://github.com/lz4/lz4 + Copyright 2023 + Yann Collet + Adrien Grand - Jodah Expiring Map - high performance thread-safe map that expires entries [The Apache Software License, Version 2.0] https://github.com/jhalterman/expiringmap - Copyright 2022 + Copyright 2023 Jonathan Halterman - - kotlin-logging - Lightweight logging framework for Kotlin - [The Apache Software License, Version 2.0] - https://github.com/MicroUtils/kotlin-logging - Copyright 2022 - Ohad Shai - - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch - - Kotlin - [The Apache Software License, Version 2.0] https://github.com/JetBrains/kotlin @@ -103,35 +114,25 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - ByteUtilties - Byte manipulation and Unsigned Number Utilities + - SLF4J - Simple facade or abstraction for various logging frameworks + [MIT License] + https://www.slf4j.org + Copyright 2023 + QOS.ch + + - ByteUtilities - Byte manipulation and SHA/xxHash utilities [The Apache Software License, Version 2.0] https://git.dorkbox.com/dorkbox/ByteUtilities - Copyright 2022 + Copyright 2023 Dorkbox LLC Extra license information - - Byte Utils (UByte, UInteger, ULong, Unsigned, UNumber, UShort) - - [The Apache Software License, Version 2.0] - https://github.com/jOOQ/jOOQ/tree/master/jOOQ/src/main/java/org/jooq/types - Copyright 2017 - Data Geekery GmbH (http://www.datageekery.com) - Lukas Eder - Ed Schaller - Jens Nerche - Ivan Sokolov - - Kryo Serialization - [BSD 3-Clause License] https://github.com/EsotericSoftware/kryo Copyright 2020 Nathan Sweet - - Kotlin Hex - - [MIT License] - https://github.com/komputing/KHex - Copyright 2017 - ligi - - Base58 - [The Apache Software License, Version 2.0] https://bitcoinj.github.io @@ -152,14 +153,14 @@ - Netty - An event-driven asynchronous network application framework [The Apache Software License, Version 2.0] https://netty.io - Copyright 2022 + Copyright 2023 The Netty Project Contributors. See source NOTICE - Kryo - Fast and efficient binary object graph serialization framework for Java [BSD 3-Clause License] https://github.com/EsotericSoftware/kryo - Copyright 2022 + Copyright 2023 Nathan Sweet Extra license information @@ -168,9 +169,9 @@ https://github.com/EsotericSoftware/reflectasm Nathan Sweet - - Objenesis - + - Objenesis - [The Apache Software License, Version 2.0] - http://objenesis.org + https://github.com/easymock/objenesis Objenesis Team and all contributors - MinLog-SLF4J - @@ -178,6 +179,20 @@ https://github.com/EsotericSoftware/minlog Nathan Sweet + - LZ4 and xxHash - LZ4 compression for Java, based on Yann Collet's work + [The Apache Software License, Version 2.0] + https://github.com/lz4/lz4 + Copyright 2023 + Yann Collet + Adrien Grand + + - XZ for Java - Complete implementation of XZ data compression in pure Java + [Public Domain, per Creative Commons CC0] + https://tukaani.org/xz/java.html + Copyright 2023 + Lasse Collin + Igor Pavlov + - Updates - Software Update Management [The Apache Software License, Version 2.0] https://git.dorkbox.com/dorkbox/Updates @@ -193,19 +208,122 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Collections - Niche collections to augment what is already available. + - ClassUtils - Class helpers and utilities for managing class hierarchies. [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - Copyright 2022 + https://git.dorkbox.com/dorkbox/ClassUtils + Copyright 2023 Dorkbox LLC Extra license information - - AhoCorasickDoubleArrayTrie - Niche collections to augment what is already available. + - TypeTools - A simple, zero-dependency library for working with types. Supports Java 1.6+ and Android. [The Apache Software License, Version 2.0] - https://github.com/hankcs/AhoCorasickDoubleArrayTrie - Copyright 2018 - hankcs + https://github.com/jhalterman/typetools + Copyright 2023 + Jonathan Halterman and friends + + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - Collections - Collection types and utilities to enhance the default collections. + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Collections + Copyright 2023 + Dorkbox LLC + + Extra license information + - Bias, BinarySearch - + [MIT License] + https://git.dorkbox.com/dorkbox/Collections + https://github.com/timboudreau/util + Copyright 2013 + Tim Boudreau + + - ConcurrentEntry - + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Collections + Copyright 2016 + bennidi + dorkbox + + - Collection Utilities (Array, ArrayMap, BooleanArray, ByteArray, CharArray, FloatArray, IdentityMap, IntArray, IntFloatMap, IntIntMap, IntMap, IntSet, LongArray, LongMap, ObjectFloatMap, ObjectIntMap, ObjectMap, ObjectSet, OrderedMap, OrderedSet) - + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Collections + https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils + Copyright 2011 + LibGDX + Mario Zechner (badlogicgames@gmail.com) + Nathan Sweet (nathan.sweet@gmail.com) + + - Predicate - + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Collections + https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils + Copyright 2011 + LibGDX + Mario Zechner (badlogicgames@gmail.com) + Nathan Sweet (nathan.sweet@gmail.com) + xoppa + + - Select, QuickSelect - + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Collections + https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils + Copyright 2011 + LibGDX + Mario Zechner (badlogicgames@gmail.com) + Nathan Sweet (nathan.sweet@gmail.com) + Jon Renner + + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - Updates - Software Update Management + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 + Dorkbox LLC + + Extra license information + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - Updates - Software Update Management + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 + Dorkbox LLC + + Extra license information + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - Collections - Collection types and utilities to enhance the default collections. + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Collections + Copyright 2023 + Dorkbox LLC + Extra license information - Bias, BinarySearch - [MIT License] https://git.dorkbox.com/dorkbox/Collections @@ -249,19 +367,6 @@ Nathan Sweet (nathan.sweet@gmail.com) Jon Renner - - TimSort, ComparableTimSort - - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils - Copyright 2008 - The Android Open Source Project - - - ConcurrentWeakIdentityHashMap - Concurrent WeakIdentity HashMap - [The Apache Software License, Version 2.0] - https://github.com/spring-projects/spring-loaded/blob/master/springloaded/src/main/java/org/springsource/loaded/support/ConcurrentWeakIdentityHashMap.java - Copyright 2016 - zhanhb - - Kotlin - [The Apache Software License, Version 2.0] https://github.com/JetBrains/kotlin @@ -285,63 +390,20 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - MinLog - Drop-in replacement for MinLog to log through SLF4j. - [BSD 3-Clause License] - https://git.dorkbox.com/dorkbox/MinLog-SLF4J - https://github.com/EsotericSoftware/minlog - Copyright 2021 - Dorkbox LLC - Nathan Sweet - Dan Brown - - Extra license information - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2021 - QOS.ch - - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - NetworkDNS - + - HexUtilities - Hex conversion utilities for Strings, Collections, ByteArrays, Unsigned numbers, and numbers [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/NetworkDNS - Copyright 2022 + https://git.dorkbox.com/dorkbox/HexUtilities + Copyright 2023 Dorkbox LLC - High-performance and event-driven/reactive DNS stack for Java 8+ Extra license information - - XBill DNS - - [BSD 2-Clause "Simplified" or "FreeBSD" license] - https://github.com/dnsjava/dnsjava - Copyright 2011 - Brian Wellington - - - Netty - An event-driven asynchronous network application framework + - Hex utility methods - [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/HexUtilities https://netty.io - Copyright 2022 + https://github.com/netty/netty/blob/4.1/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java + Copyright 2014 The Netty Project - Contributors. See source NOTICE - - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch - Kotlin - [The Apache Software License, Version 2.0] @@ -351,13 +413,28 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment. + - ByteUtilities - Byte manipulation and SHA/xxHash utilities [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/OS - Copyright 2022 + https://git.dorkbox.com/dorkbox/ByteUtilities + Copyright 2023 Dorkbox LLC Extra license information + - Kryo Serialization - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/kryo + Copyright 2020 + Nathan Sweet + + - Base58 - + [The Apache Software License, Version 2.0] + https://bitcoinj.github.io + https://github.com/komputing/KBase58 + Copyright 2018 + Google Inc + Andreas Schildbach + ligi + - Kotlin - [The Apache Software License, Version 2.0] https://github.com/JetBrains/kotlin @@ -366,6 +443,49 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + - Netty - An event-driven asynchronous network application framework + [The Apache Software License, Version 2.0] + https://netty.io + Copyright 2023 + The Netty Project + Contributors. See source NOTICE + + - Kryo - Fast and efficient binary object graph serialization framework for Java + [BSD 3-Clause License] + https://github.com/EsotericSoftware/kryo + Copyright 2023 + Nathan Sweet + + Extra license information + - ReflectASM - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/reflectasm + Nathan Sweet + + - Objenesis - + [The Apache Software License, Version 2.0] + https://github.com/easymock/objenesis + Objenesis Team and all contributors + + - MinLog-SLF4J - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/minlog + Nathan Sweet + + - LZ4 and xxHash - LZ4 compression for Java, based on Yann Collet's work + [The Apache Software License, Version 2.0] + https://github.com/lz4/lz4 + Copyright 2023 + Yann Collet + Adrien Grand + + - XZ for Java - Complete implementation of XZ data compression in pure Java + [Public Domain, per Creative Commons CC0] + https://tukaani.org/xz/java.html + Copyright 2023 + Lasse Collin + Igor Pavlov + - Updates - Software Update Management [The Apache Software License, Version 2.0] https://git.dorkbox.com/dorkbox/Updates @@ -381,125 +501,53 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Utilities - Utilities for use within Java projects + - Updates - Software Update Management [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Utilities - Copyright 2022 + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 Dorkbox LLC Extra license information - - MersenneTwisterFast - - [BSD 3-Clause License] - https://git.dorkbox.com/dorkbox/Utilities - Copyright 2003 - Sean Luke - Michael Lecuyer (portions Copyright 1993 - - - FileUtil (code from FilenameUtils.java for normalize + dependencies) - + - Kotlin - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Utilities - http://commons.apache.org/proper/commons-io/ - Copyright 2013 - The Apache Software Foundation - Kevin A. Burton - Scott Sanders - Daniel Rall - Christoph.Reck - Peter Donald - Jeff Turner - Matthew Hawthorne - Martin Cooper - Jeremias Maerki - Stephen Colebourne - - - FastThreadLocal - - [BSD 3-Clause License] - https://git.dorkbox.com/dorkbox/Utilities - https://github.com/LWJGL/lwjgl3/blob/5819c9123222f6ce51f208e022cb907091dd8023/modules/core/src/main/java/org/lwjgl/system/FastThreadLocal.java - https://github.com/riven8192/LibStruct/blob/master/src/net/indiespot/struct/runtime/FastThreadLocal.java - Copyright 2014 - Lightweight Java Game Library Project - Riven - - - Base64Fast - - [BSD 3-Clause License] - https://git.dorkbox.com/dorkbox/Utilities - http://migbase64.sourceforge.net/ - Copyright 2004 - Mikael Grev, MiG InfoCom AB. (base64@miginfocom.com) - - - BCrypt - - [BSD 2-Clause "Simplified" or "FreeBSD" license] - https://git.dorkbox.com/dorkbox/Utilities - http://www.mindrot.org/projects/jBCrypt - Copyright 2006 - Damien Miller (djm@mindrot.org) - GWT modified version - - - Modified hex conversion utility methods - - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Utilities - https://netty.io - Copyright 2014 - The Netty Project - - - Retrofit - A type-safe HTTP client for Android and Java - [The Apache Software License, Version 2.0] - https://github.com/square/retrofit + https://github.com/JetBrains/kotlin Copyright 2020 - Square, Inc - - - Resource Listing - Listing the contents of a resource directory - [The Apache Software License, Version 2.0] - http://www.uofr.net/~greg/java/get-resource-listing.html - Copyright 2017 - Greg Briggs - - - CommonUtils - Common utility extension functions for kotlin - [The Apache Software License, Version 2.0] - https://www.pronghorn.tech - Copyright 2017 - Pronghorn Technology LLC - Dorkbox LLC - - - UrlRewriteFilter - UrlRewriteFilter is a Java Web Filter for any J2EE compliant web application server - [BSD 3-Clause License] - https://github.com/paultuckey/urlrewritefilter - Copyright 2022 - Paul Tuckey + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support - [The Apache Software License, Version 2.0] - https://github.com/Kotlin/kotlinx.coroutines - Copyright 2022 - JetBrains s.r.o. + - JNA - Native JNA extensions for Linux, MacOS, and Windows, Java 1.8+ + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/JNA + Copyright 2023 + Dorkbox LLC - - Java Uuid Generator - A set of Java classes for working with UUIDs - [The Apache Software License, Version 2.0] - https://github.com/cowtowncoder/java-uuid-generator - Copyright 2022 - Tatu Saloranta (tatu.saloranta@iki.fi) - Contributors. See source release-notes/CREDITS + Extra license information + - JNA - Simplified native library access for Java. + [The Apache Software License, Version 2.0] + https://github.com/twall/jna + Copyright 2023 + Timothy Wall - - kotlin-logging - Lightweight logging framework for Kotlin - [The Apache Software License, Version 2.0] - https://github.com/MicroUtils/kotlin-logging - Copyright 2022 - Ohad Shai + - JNA-Platform - Mappings for a number of commonly used platform functions + [The Apache Software License, Version 2.0] + https://github.com/twall/jna + Copyright 2023 + Timothy Wall - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch + - SLF4J - Simple facade or abstraction for various logging frameworks + [MIT License] + https://www.slf4j.org + Copyright 2023 + QOS.ch - - XZ for Java - Complete implementation of XZ data compression in pure Java - [Public Domain, per Creative Commons CC0] - https://tukaani.org/xz/java.html - Copyright 2022 - Lasse Collin - Igor Pavlov + - OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment. + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/OS + Copyright 2023 + Dorkbox LLC + Extra license information - Kotlin - [The Apache Software License, Version 2.0] https://github.com/JetBrains/kotlin @@ -508,112 +556,13 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - JNA - Simplified native library access for Java. - [The Apache Software License, Version 2.0] - https://github.com/twall/jna - Copyright 2022 - Timothy Wall - - - JNA-Platform - Mappings for a number of commonly used platform functions - [The Apache Software License, Version 2.0] - https://github.com/twall/jna - Copyright 2022 - Timothy Wall - - - Netty - An event-driven asynchronous network application framework - [The Apache Software License, Version 2.0] - https://netty.io - Copyright 2022 - The Netty Project - Contributors. See source NOTICE - - - Bouncy Castle Crypto - Lightweight cryptography API and JCE Extension - [The Apache Software License, Version 2.0] - http://www.bouncycastle.org - Copyright 2022 - The Legion of the Bouncy Castle Inc - - - Lightweight Java Game Library - Java library that enables cross-platform access to popular native APIs - [BSD 3-Clause License] - https://github.com/LWJGL/lwjgl3 - Copyright 2022 - Lightweight Java Game Library - - - TypeTools - A simple, zero-dependency library for working with types. Supports Java 1.6+ and Android. - [The Apache Software License, Version 2.0] - https://github.com/jhalterman/typetools - Copyright 2022 - Jonathan Halterman and friends - - - Collections - Niche collections to augment what is already available. + - Updates - Software Update Management [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - Copyright 2022 + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 Dorkbox LLC Extra license information - - AhoCorasickDoubleArrayTrie - Niche collections to augment what is already available. - [The Apache Software License, Version 2.0] - https://github.com/hankcs/AhoCorasickDoubleArrayTrie - Copyright 2018 - hankcs - - - Bias, BinarySearch - - [MIT License] - https://git.dorkbox.com/dorkbox/Collections - https://github.com/timboudreau/util - Copyright 2013 - Tim Boudreau - - - ConcurrentEntry - - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - Copyright 2016 - bennidi - dorkbox - - - Collection Utilities (Array, ArrayMap, BooleanArray, ByteArray, CharArray, FloatArray, IdentityMap, IntArray, IntFloatMap, IntIntMap, IntMap, IntSet, LongArray, LongMap, ObjectFloatMap, ObjectIntMap, ObjectMap, ObjectSet, OrderedMap, OrderedSet) - - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils - Copyright 2011 - LibGDX - Mario Zechner (badlogicgames@gmail.com) - Nathan Sweet (nathan.sweet@gmail.com) - - - Predicate - - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils - Copyright 2011 - LibGDX - Mario Zechner (badlogicgames@gmail.com) - Nathan Sweet (nathan.sweet@gmail.com) - xoppa - - - Select, QuickSelect - - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils - Copyright 2011 - LibGDX - Mario Zechner (badlogicgames@gmail.com) - Nathan Sweet (nathan.sweet@gmail.com) - Jon Renner - - - TimSort, ComparableTimSort - - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils - Copyright 2008 - The Android Open Source Project - - - ConcurrentWeakIdentityHashMap - Concurrent WeakIdentity HashMap - [The Apache Software License, Version 2.0] - https://github.com/spring-projects/spring-loaded/blob/master/springloaded/src/main/java/org/springsource/loaded/support/ConcurrentWeakIdentityHashMap.java - Copyright 2016 - zhanhb - - Kotlin - [The Apache Software License, Version 2.0] https://github.com/JetBrains/kotlin @@ -622,307 +571,144 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + - Updates - Software Update Management + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 + Dorkbox LLC - - Executor - Shell, JVM, and SSH command execution on Linux, MacOS, or Windows for Java 8+ + Extra license information + - Kotlin - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Executor - Copyright 2022 - Dorkbox LLC - - Extra license information - - ZT Process Executor - - [The Apache Software License, Version 2.0] - https://github.com/zeroturnaround/zt-exec - Copyright 2014 - ZeroTurnaround LLC - - - Apache Commons Exec - - [The Apache Software License, Version 2.0] - https://commons.apache.org/proper/commons-exec/ - Copyright 2014 - The Apache Software Foundation - - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support - [The Apache Software License, Version 2.0] - https://github.com/Kotlin/kotlinx.coroutines - Copyright 2022 - JetBrains s.r.o. - - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch - - - Logback - Logback is a logging framework for Java applications - [The Apache Software License, Version 2.0] - http://logback.qos.ch - Copyright 2022 - QOS.ch - - - SSHJ - SSHv2 library for Java - [The Apache Software License, Version 2.0] - https://github.com/hierynomus/sshj - Copyright 2022 - Jeroen van Erp - SSHJ Contributors - - Extra license information - - Apache MINA - - [The Apache Software License, Version 2.0] - https://mina.apache.org/sshd-project/ - The Apache Software Foundation - - - Apache Commons-Net - - [The Apache Software License, Version 2.0] - https://commons.apache.org/proper/commons-net/ - The Apache Software Foundation - - - JZlib - - [The Apache Software License, Version 2.0] - http://www.jcraft.com/jzlib - Atsuhiko Yamanaka - JCraft, Inc. - - - Bouncy Castle Crypto - - [The Apache Software License, Version 2.0] - http://www.bouncycastle.org - The Legion of the Bouncy Castle Inc + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - ed25519-java - - [Public Domain, per Creative Commons CC0] - https://github.com/str4d/ed25519-java - https://github.com/str4d + - MinLog - Drop-in replacement for MinLog to log through SLF4j. + [BSD 3-Clause License] + https://git.dorkbox.com/dorkbox/MinLog-SLF4J + https://github.com/EsotericSoftware/minlog + Copyright 2023 + Dorkbox LLC + Nathan Sweet + Dan Brown - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC + Extra license information + - SLF4J - Simple facade or abstraction for various logging frameworks + [MIT License] + https://www.slf4j.org + Copyright 2023 + QOS.ch - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + - Updates - Software Update Management + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 + Dorkbox LLC - - NetworkUtils - Utilities for managing network configurations, IP/MAC address conversion, and ping (via OS native commands) + Extra license information + - Kotlin - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/NetworkUtils - Copyright 2022 - Dorkbox LLC - - Extra license information - - Netty - - [The Apache Software License, Version 2.0] - https://netty.io/ - Copyright 2014 - The Netty Project - This product contains a modified portion of Netty Network Utils - - - Apache Harmony - - [The Apache Software License, Version 2.0] - http://archive.apache.org/dist/harmony/ - Copyright 2010 - The Apache Software Foundation - This product contains a modified portion of 'Apache Harmony', an open source Java SE - - - Apache HTTP Utils - - [The Apache Software License, Version 2.0] - http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/psl/ - Copyright 2010 - The Apache Software Foundation - This product contains a modified portion of 'PublicSuffixDomainFilter.java' - - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch - - - JNA - Simplified native library access for Java. - [The Apache Software License, Version 2.0] - https://github.com/twall/jna - Copyright 2022 - Timothy Wall - - - JNA-Platform - Mappings for a number of commonly used platform functions - [The Apache Software License, Version 2.0] - https://github.com/twall/jna - Copyright 2022 - Timothy Wall - - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - Executor - Shell, JVM, and SSH command execution on Linux, MacOS, or Windows for Java 8+ - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Executor - Copyright 2022 - Dorkbox LLC - - Extra license information - - ZT Process Executor - - [The Apache Software License, Version 2.0] - https://github.com/zeroturnaround/zt-exec - Copyright 2014 - ZeroTurnaround LLC - - - Apache Commons Exec - - [The Apache Software License, Version 2.0] - https://commons.apache.org/proper/commons-exec/ - Copyright 2014 - The Apache Software Foundation + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + - NetworkDNS - + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/NetworkDNS + Copyright 2023 + Dorkbox LLC + High-performance and event-driven/reactive DNS stack for Java 8+ - - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support - [The Apache Software License, Version 2.0] - https://github.com/Kotlin/kotlinx.coroutines - Copyright 2022 - JetBrains s.r.o. + Extra license information + - XBill DNS - + [BSD 2-Clause "Simplified" or "FreeBSD" license] + https://github.com/dnsjava/dnsjava + Copyright 2011 + Brian Wellington - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch + - Netty - An event-driven asynchronous network application framework + [The Apache Software License, Version 2.0] + https://netty.io + Copyright 2023 + The Netty Project + Contributors. See source NOTICE - - Logback - Logback is a logging framework for Java applications - [The Apache Software License, Version 2.0] - http://logback.qos.ch - Copyright 2022 - QOS.ch + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - SSHJ - SSHv2 library for Java - [The Apache Software License, Version 2.0] - https://github.com/hierynomus/sshj - Copyright 2022 - Jeroen van Erp - SSHJ Contributors - - Extra license information - - Apache MINA - - [The Apache Software License, Version 2.0] - https://mina.apache.org/sshd-project/ - The Apache Software Foundation - - - Apache Commons-Net - - [The Apache Software License, Version 2.0] - https://commons.apache.org/proper/commons-net/ - The Apache Software Foundation - - - JZlib - - [The Apache Software License, Version 2.0] - http://www.jcraft.com/jzlib - Atsuhiko Yamanaka - JCraft, Inc. - - - Bouncy Castle Crypto - - [The Apache Software License, Version 2.0] - http://www.bouncycastle.org - The Legion of the Bouncy Castle Inc - - - ed25519-java - - [Public Domain, per Creative Commons CC0] - https://github.com/str4d/ed25519-java - https://github.com/str4d - - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + - SLF4J - Simple facade or abstraction for various logging frameworks + [MIT License] + https://www.slf4j.org + Copyright 2023 + QOS.ch - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC + - Collections - Collection types and utilities to enhance the default collections. + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Collections + Copyright 2023 + Dorkbox LLC - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + Extra license information + - Bias, BinarySearch - + [MIT License] + https://git.dorkbox.com/dorkbox/Collections + https://github.com/timboudreau/util + Copyright 2013 + Tim Boudreau - - OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment. + - ConcurrentEntry - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/OS - Copyright 2022 - Dorkbox LLC + https://git.dorkbox.com/dorkbox/Collections + Copyright 2016 + bennidi + dorkbox - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + - Collection Utilities (Array, ArrayMap, BooleanArray, ByteArray, CharArray, FloatArray, IdentityMap, IntArray, IntFloatMap, IntIntMap, IntMap, IntSet, LongArray, LongMap, ObjectFloatMap, ObjectIntMap, ObjectMap, ObjectSet, OrderedMap, OrderedSet) - + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Collections + https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils + Copyright 2011 + LibGDX + Mario Zechner (badlogicgames@gmail.com) + Nathan Sweet (nathan.sweet@gmail.com) - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC + - Predicate - + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Collections + https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils + Copyright 2011 + LibGDX + Mario Zechner (badlogicgames@gmail.com) + Nathan Sweet (nathan.sweet@gmail.com) + xoppa - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + - Select, QuickSelect - + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Collections + https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils + Copyright 2011 + LibGDX + Mario Zechner (badlogicgames@gmail.com) + Nathan Sweet (nathan.sweet@gmail.com) + Jon Renner + + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - Updates - Software Update Management [The Apache Software License, Version 2.0] @@ -939,67 +725,70 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - NetworkUtils - Utilities for managing network configurations, IP/MAC address conversion, and ping (via OS native commands) [The Apache Software License, Version 2.0] https://git.dorkbox.com/dorkbox/NetworkUtils - Copyright 2022 + Copyright 2023 Dorkbox LLC Extra license information - Netty - [The Apache Software License, Version 2.0] - https://netty.io/ + https://netty.io Copyright 2014 The Netty Project This product contains a modified portion of Netty Network Utils - Apache Harmony - [The Apache Software License, Version 2.0] - http://archive.apache.org/dist/harmony/ + https://archive.apache.org/dist/harmony/ Copyright 2010 The Apache Software Foundation This product contains a modified portion of 'Apache Harmony', an open source Java SE + - Mozilla Public Suffix List - + [Mozilla Public License 2.0] + https://publicsuffix.org/list/public_suffix_list.dat + Copyright 2010 + The Apache Software Foundation + - Apache HTTP Utils - [The Apache Software License, Version 2.0] - http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/psl/ + https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/psl/ Copyright 2010 The Apache Software Foundation This product contains a modified portion of 'PublicSuffixDomainFilter.java' - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org + - UrlRewriteFilter - UrlRewriteFilter is a Java Web Filter for any J2EE compliant web application server + [BSD 3-Clause License] + https://github.com/paultuckey/urlrewritefilter Copyright 2022 - QOS.ch + Paul Tuckey + + - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support + [The Apache Software License, Version 2.0] + https://github.com/Kotlin/kotlinx.coroutines + Copyright 2023 + JetBrains s.r.o. - JNA - Simplified native library access for Java. [The Apache Software License, Version 2.0] https://github.com/twall/jna - Copyright 2022 + Copyright 2023 Timothy Wall - JNA-Platform - Mappings for a number of commonly used platform functions [The Apache Software License, Version 2.0] https://github.com/twall/jna - Copyright 2022 + Copyright 2023 Timothy Wall + - SLF4J - Simple facade or abstraction for various logging frameworks + [MIT License] + https://www.slf4j.org + Copyright 2023 + QOS.ch + - Kotlin - [The Apache Software License, Version 2.0] https://github.com/JetBrains/kotlin @@ -1011,7 +800,7 @@ - Executor - Shell, JVM, and SSH command execution on Linux, MacOS, or Windows for Java 8+ [The Apache Software License, Version 2.0] https://git.dorkbox.com/dorkbox/Executor - Copyright 2022 + Copyright 2023 Dorkbox LLC Extra license information @@ -1038,25 +827,25 @@ - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support [The Apache Software License, Version 2.0] https://github.com/Kotlin/kotlinx.coroutines - Copyright 2022 + Copyright 2023 JetBrains s.r.o. - SLF4J - Simple facade or abstraction for various logging frameworks [MIT License] - http://www.slf4j.org - Copyright 2022 + https://www.slf4j.org + Copyright 2023 QOS.ch - Logback - Logback is a logging framework for Java applications [The Apache Software License, Version 2.0] - http://logback.qos.ch - Copyright 2022 + https://logback.qos.ch + Copyright 2023 QOS.ch - SSHJ - SSHv2 library for Java [The Apache Software License, Version 2.0] https://github.com/hierynomus/sshj - Copyright 2022 + Copyright 2023 Jeroen van Erp SSHJ Contributors @@ -1073,13 +862,13 @@ - JZlib - [The Apache Software License, Version 2.0] - http://www.jcraft.com/jzlib + https://github.com/ymnk/jzlib Atsuhiko Yamanaka JCraft, Inc. - Bouncy Castle Crypto - [The Apache Software License, Version 2.0] - http://www.bouncycastle.org + https://www.bouncycastle.org The Legion of the Bouncy Castle Inc - ed25519-java - @@ -1102,215 +891,25 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - NetworkUtils - Utilities for managing network configurations, IP/MAC address conversion, and ping (via OS native commands) - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/NetworkUtils - Copyright 2022 - Dorkbox LLC - - Extra license information - - Netty - - [The Apache Software License, Version 2.0] - https://netty.io/ - Copyright 2014 - The Netty Project - This product contains a modified portion of Netty Network Utils - - - Apache Harmony - - [The Apache Software License, Version 2.0] - http://archive.apache.org/dist/harmony/ - Copyright 2010 - The Apache Software Foundation - This product contains a modified portion of 'Apache Harmony', an open source Java SE - - - Apache HTTP Utils - - [The Apache Software License, Version 2.0] - http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/psl/ - Copyright 2010 - The Apache Software Foundation - This product contains a modified portion of 'PublicSuffixDomainFilter.java' - - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch - - - JNA - Simplified native library access for Java. - [The Apache Software License, Version 2.0] - https://github.com/twall/jna - Copyright 2022 - Timothy Wall - - - JNA-Platform - Mappings for a number of commonly used platform functions - [The Apache Software License, Version 2.0] - https://github.com/twall/jna - Copyright 2022 - Timothy Wall - - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - Executor - Shell, JVM, and SSH command execution on Linux, MacOS, or Windows for Java 8+ - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Executor - Copyright 2022 - Dorkbox LLC - - Extra license information - - ZT Process Executor - - [The Apache Software License, Version 2.0] - https://github.com/zeroturnaround/zt-exec - Copyright 2014 - ZeroTurnaround LLC - - - Apache Commons Exec - - [The Apache Software License, Version 2.0] - https://commons.apache.org/proper/commons-exec/ - Copyright 2014 - The Apache Software Foundation - - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support - [The Apache Software License, Version 2.0] - https://github.com/Kotlin/kotlinx.coroutines - Copyright 2022 - JetBrains s.r.o. - - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch - - - Logback - Logback is a logging framework for Java applications - [The Apache Software License, Version 2.0] - http://logback.qos.ch - Copyright 2022 - QOS.ch - - - SSHJ - SSHv2 library for Java - [The Apache Software License, Version 2.0] - https://github.com/hierynomus/sshj - Copyright 2022 - Jeroen van Erp - SSHJ Contributors - - Extra license information - - Apache MINA - - [The Apache Software License, Version 2.0] - https://mina.apache.org/sshd-project/ - The Apache Software Foundation - - - Apache Commons-Net - - [The Apache Software License, Version 2.0] - https://commons.apache.org/proper/commons-net/ - The Apache Software Foundation - - - JZlib - - [The Apache Software License, Version 2.0] - http://www.jcraft.com/jzlib - Atsuhiko Yamanaka - JCraft, Inc. - - - Bouncy Castle Crypto - - [The Apache Software License, Version 2.0] - http://www.bouncycastle.org - The Legion of the Bouncy Castle Inc - - - ed25519-java - - [Public Domain, per Creative Commons CC0] - https://github.com/str4d/ed25519-java - https://github.com/str4d - - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - ObjectPool - Fast, lightweight, and compatible blocking/non-blocking/soft-reference object pool for Java 8+ - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/ObjectPool - Copyright 2022 - Dorkbox LLC - - Extra license information - - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support - [The Apache Software License, Version 2.0] - https://github.com/Kotlin/kotlinx.coroutines - Copyright 2022 - JetBrains s.r.o. - - - Conversant Disruptor - Disruptor is the highest performing intra-thread transfer mechanism available in Java. - [The Apache Software License, Version 2.0] - https://github.com/conversant/disruptor - Copyright 2022 - Conversant, Inc - - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + - Updates - Software Update Management + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 + Dorkbox LLC - - Updates - Software Update Management + Extra license information + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment. [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 + https://git.dorkbox.com/dorkbox/OS + Copyright 2023 Dorkbox LLC Extra license information @@ -1322,20 +921,20 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment. - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/OS - Copyright 2022 - Dorkbox LLC + - Updates - Software Update Management + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 + Dorkbox LLC - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + Extra license information + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - Updates - Software Update Management [The Apache Software License, Version 2.0] @@ -1352,112 +951,70 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Serializers - Kryo based serializers + - NetworkUtils - Utilities for managing network configurations, IP/MAC address conversion, and ping (via OS native commands) [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Serializers - Copyright 2022 + https://git.dorkbox.com/dorkbox/NetworkUtils + Copyright 2023 Dorkbox LLC Extra license information - - Kryo Serializers - + - Netty - [The Apache Software License, Version 2.0] - https://github.com/magro/kryo-serializers - Copyright 2021 - Martin Grotzke - Rafael Winterhalter + https://netty.io + Copyright 2014 + The Netty Project + This product contains a modified portion of Netty Network Utils - - Kotlin - + - Apache Harmony - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - Kryo - Fast and efficient binary object graph serialization framework for Java - [BSD 3-Clause License] - https://github.com/EsotericSoftware/kryo - Copyright 2022 - Nathan Sweet - - Extra license information - - ReflectASM - - [BSD 3-Clause License] - https://github.com/EsotericSoftware/reflectasm - Nathan Sweet - - - Objenesis - - [The Apache Software License, Version 2.0] - http://objenesis.org - Objenesis Team and all contributors + https://archive.apache.org/dist/harmony/ + Copyright 2010 + The Apache Software Foundation + This product contains a modified portion of 'Apache Harmony', an open source Java SE - - MinLog-SLF4J - - [BSD 3-Clause License] - https://github.com/EsotericSoftware/minlog - Nathan Sweet + - Mozilla Public Suffix List - + [Mozilla Public License 2.0] + https://publicsuffix.org/list/public_suffix_list.dat + Copyright 2010 + The Apache Software Foundation - - Bouncy Castle Crypto - Lightweight cryptography API and JCE Extension + - Apache HTTP Utils - [The Apache Software License, Version 2.0] - http://www.bouncycastle.org + https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/psl/ + Copyright 2010 + The Apache Software Foundation + This product contains a modified portion of 'PublicSuffixDomainFilter.java' + + - UrlRewriteFilter - UrlRewriteFilter is a Java Web Filter for any J2EE compliant web application server + [BSD 3-Clause License] + https://github.com/paultuckey/urlrewritefilter Copyright 2022 - The Legion of the Bouncy Castle Inc + Paul Tuckey - - Updates - Software Update Management + - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + https://github.com/Kotlin/kotlinx.coroutines + Copyright 2023 + JetBrains s.r.o. - - Storage - Storage system for Java - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Storage - Copyright 2022 - Dorkbox LLC + - JNA - Simplified native library access for Java. + [The Apache Software License, Version 2.0] + https://github.com/twall/jna + Copyright 2023 + Timothy Wall - Extra license information - - kotlin-logging - Lightweight logging framework for Kotlin + - JNA-Platform - Mappings for a number of commonly used platform functions [The Apache Software License, Version 2.0] - https://github.com/MicroUtils/kotlin-logging - Copyright 2022 - Ohad Shai + https://github.com/twall/jna + Copyright 2023 + Timothy Wall - SLF4J - Simple facade or abstraction for various logging frameworks [MIT License] - http://www.slf4j.org - Copyright 2022 + https://www.slf4j.org + Copyright 2023 QOS.ch - - Kryo - Fast and efficient binary object graph serialization framework for Java - [BSD 3-Clause License] - https://github.com/EsotericSoftware/kryo - Copyright 2022 - Nathan Sweet - - Extra license information - - ReflectASM - - [BSD 3-Clause License] - https://github.com/EsotericSoftware/reflectasm - Nathan Sweet - - - Objenesis - - [The Apache Software License, Version 2.0] - http://objenesis.org - Objenesis Team and all contributors - - - MinLog-SLF4J - - [BSD 3-Clause License] - https://github.com/EsotericSoftware/minlog - Nathan Sweet - - Kotlin - [The Apache Software License, Version 2.0] https://github.com/JetBrains/kotlin @@ -1466,124 +1023,24 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - ByteUtilties - Byte manipulation and Unsigned Number Utilities + - Executor - Shell, JVM, and SSH command execution on Linux, MacOS, or Windows for Java 8+ [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/ByteUtilities - Copyright 2022 + https://git.dorkbox.com/dorkbox/Executor + Copyright 2023 Dorkbox LLC Extra license information - - Byte Utils (UByte, UInteger, ULong, Unsigned, UNumber, UShort) - - [The Apache Software License, Version 2.0] - https://github.com/jOOQ/jOOQ/tree/master/jOOQ/src/main/java/org/jooq/types - Copyright 2017 - Data Geekery GmbH (http://www.datageekery.com) - Lukas Eder - Ed Schaller - Jens Nerche - Ivan Sokolov - - - Kryo Serialization - - [BSD 3-Clause License] - https://github.com/EsotericSoftware/kryo - Copyright 2020 - Nathan Sweet - - - Kotlin Hex - - [MIT License] - https://github.com/komputing/KHex - Copyright 2017 - ligi - - - Base58 - - [The Apache Software License, Version 2.0] - https://bitcoinj.github.io - https://github.com/komputing/KBase58 - Copyright 2018 - Google Inc - Andreas Schildbach - ligi - - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - Netty - An event-driven asynchronous network application framework - [The Apache Software License, Version 2.0] - https://netty.io - Copyright 2022 - The Netty Project - Contributors. See source NOTICE - - - Kryo - Fast and efficient binary object graph serialization framework for Java - [BSD 3-Clause License] - https://github.com/EsotericSoftware/kryo - Copyright 2022 - Nathan Sweet - - Extra license information - - ReflectASM - - [BSD 3-Clause License] - https://github.com/EsotericSoftware/reflectasm - Nathan Sweet - - - Objenesis - - [The Apache Software License, Version 2.0] - http://objenesis.org - Objenesis Team and all contributors - - - MinLog-SLF4J - - [BSD 3-Clause License] - https://github.com/EsotericSoftware/minlog - Nathan Sweet - - - Updates - Software Update Management + - ZT Process Executor - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - Serializers - Kryo based serializers - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Serializers - Copyright 2022 - Dorkbox LLC + https://github.com/zeroturnaround/zt-exec + Copyright 2014 + ZeroTurnaround LLC - Extra license information - - Kryo Serializers - + - Apache Commons Exec - [The Apache Software License, Version 2.0] - https://github.com/magro/kryo-serializers - Copyright 2021 - Martin Grotzke - Rafael Winterhalter + https://commons.apache.org/proper/commons-exec/ + Copyright 2014 + The Apache Software Foundation - Kotlin - [The Apache Software License, Version 2.0] @@ -1593,33 +1050,57 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Kryo - Fast and efficient binary object graph serialization framework for Java - [BSD 3-Clause License] - https://github.com/EsotericSoftware/kryo - Copyright 2022 - Nathan Sweet + - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support + [The Apache Software License, Version 2.0] + https://github.com/Kotlin/kotlinx.coroutines + Copyright 2023 + JetBrains s.r.o. + + - SLF4J - Simple facade or abstraction for various logging frameworks + [MIT License] + https://www.slf4j.org + Copyright 2023 + QOS.ch + + - Logback - Logback is a logging framework for Java applications + [The Apache Software License, Version 2.0] + https://logback.qos.ch + Copyright 2023 + QOS.ch + + - SSHJ - SSHv2 library for Java + [The Apache Software License, Version 2.0] + https://github.com/hierynomus/sshj + Copyright 2023 + Jeroen van Erp + SSHJ Contributors Extra license information - - ReflectASM - - [BSD 3-Clause License] - https://github.com/EsotericSoftware/reflectasm - Nathan Sweet + - Apache MINA - + [The Apache Software License, Version 2.0] + https://mina.apache.org/sshd-project/ + The Apache Software Foundation - - Objenesis - + - Apache Commons-Net - [The Apache Software License, Version 2.0] - http://objenesis.org - Objenesis Team and all contributors + https://commons.apache.org/proper/commons-net/ + The Apache Software Foundation - - MinLog-SLF4J - - [BSD 3-Clause License] - https://github.com/EsotericSoftware/minlog - Nathan Sweet + - JZlib - + [The Apache Software License, Version 2.0] + https://github.com/ymnk/jzlib + Atsuhiko Yamanaka + JCraft, Inc. - - Bouncy Castle Crypto - Lightweight cryptography API and JCE Extension - [The Apache Software License, Version 2.0] - http://www.bouncycastle.org - Copyright 2022 - The Legion of the Bouncy Castle Inc + - Bouncy Castle Crypto - + [The Apache Software License, Version 2.0] + https://www.bouncycastle.org + The Legion of the Bouncy Castle Inc + + - ed25519-java - + [Public Domain, per Creative Commons CC0] + https://github.com/str4d/ed25519-java + https://github.com/str4d - Updates - Software Update Management [The Apache Software License, Version 2.0] @@ -1636,25 +1117,13 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - ObjectPool - Fast, lightweight, and compatible blocking/non-blocking/soft-reference object pool for Java 8+ + - Updates - Software Update Management [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/ObjectPool - Copyright 2022 + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 Dorkbox LLC Extra license information - - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support - [The Apache Software License, Version 2.0] - https://github.com/Kotlin/kotlinx.coroutines - Copyright 2022 - JetBrains s.r.o. - - - Conversant Disruptor - Disruptor is the highest performing intra-thread transfer mechanism available in Java. - [The Apache Software License, Version 2.0] - https://github.com/conversant/disruptor - Copyright 2022 - Conversant, Inc - - Kotlin - [The Apache Software License, Version 2.0] https://github.com/JetBrains/kotlin @@ -1663,56 +1132,10 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - MinLog - Drop-in replacement for MinLog to log through SLF4j. - [BSD 3-Clause License] - https://git.dorkbox.com/dorkbox/MinLog-SLF4J - https://github.com/EsotericSoftware/minlog - Copyright 2022 - Dorkbox LLC - Nathan Sweet - Dan Brown - - Extra license information - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch - - - Updates - Software Update Management - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC - - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - - Updates - Software Update Management + - OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment. [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 + https://git.dorkbox.com/dorkbox/OS + Copyright 2023 Dorkbox LLC Extra license information @@ -1724,124 +1147,114 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Utilities - Utilities for use within Java projects + - Updates - Software Update Management + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 + Dorkbox LLC + + Extra license information + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - Serializers - Kryo based serializers [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Utilities - Copyright 2022 + https://git.dorkbox.com/dorkbox/Serializers + Copyright 2023 Dorkbox LLC Extra license information - - MersenneTwisterFast - - [BSD 3-Clause License] - https://git.dorkbox.com/dorkbox/Utilities - Copyright 2003 - Sean Luke - Michael Lecuyer (portions Copyright 1993 - - - FileUtil (code from FilenameUtils.java for normalize + dependencies) - + - Kryo Serializers - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Utilities - http://commons.apache.org/proper/commons-io/ - Copyright 2013 - The Apache Software Foundation - Kevin A. Burton - Scott Sanders - Daniel Rall - Christoph.Reck - Peter Donald - Jeff Turner - Matthew Hawthorne - Martin Cooper - Jeremias Maerki - Stephen Colebourne + https://github.com/magro/kryo-serializers + Copyright 2021 + Martin Grotzke + Rafael Winterhalter - - FastThreadLocal - - [BSD 3-Clause License] - https://git.dorkbox.com/dorkbox/Utilities - https://github.com/LWJGL/lwjgl3/blob/5819c9123222f6ce51f208e022cb907091dd8023/modules/core/src/main/java/org/lwjgl/system/FastThreadLocal.java - https://github.com/riven8192/LibStruct/blob/master/src/net/indiespot/struct/runtime/FastThreadLocal.java - Copyright 2014 - Lightweight Java Game Library Project - Riven + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Base64Fast - + - Kryo - Fast and efficient binary object graph serialization framework for Java [BSD 3-Clause License] - https://git.dorkbox.com/dorkbox/Utilities - http://migbase64.sourceforge.net/ - Copyright 2004 - Mikael Grev, MiG InfoCom AB. (base64@miginfocom.com) + https://github.com/EsotericSoftware/kryo + Copyright 2023 + Nathan Sweet - - BCrypt - - [BSD 2-Clause "Simplified" or "FreeBSD" license] - https://git.dorkbox.com/dorkbox/Utilities - http://www.mindrot.org/projects/jBCrypt - Copyright 2006 - Damien Miller (djm@mindrot.org) - GWT modified version + Extra license information + - ReflectASM - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/reflectasm + Nathan Sweet - - Modified hex conversion utility methods - - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Utilities - https://netty.io - Copyright 2014 - The Netty Project + - Objenesis - + [The Apache Software License, Version 2.0] + https://github.com/easymock/objenesis + Objenesis Team and all contributors - - Retrofit - A type-safe HTTP client for Android and Java - [The Apache Software License, Version 2.0] - https://github.com/square/retrofit - Copyright 2020 - Square, Inc + - MinLog-SLF4J - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/minlog + Nathan Sweet - - Resource Listing - Listing the contents of a resource directory + - Bouncy Castle Crypto - Lightweight cryptography API and JCE Extension [The Apache Software License, Version 2.0] - http://www.uofr.net/~greg/java/get-resource-listing.html - Copyright 2017 - Greg Briggs + https://www.bouncycastle.org + Copyright 2023 + The Legion of the Bouncy Castle Inc - - CommonUtils - Common utility extension functions for kotlin + - Updates - Software Update Management [The Apache Software License, Version 2.0] - https://www.pronghorn.tech - Copyright 2017 - Pronghorn Technology LLC + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 Dorkbox LLC - - UrlRewriteFilter - UrlRewriteFilter is a Java Web Filter for any J2EE compliant web application server - [BSD 3-Clause License] - https://github.com/paultuckey/urlrewritefilter - Copyright 2022 - Paul Tuckey + Extra license information + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support - [The Apache Software License, Version 2.0] - https://github.com/Kotlin/kotlinx.coroutines - Copyright 2022 - JetBrains s.r.o. + - Storage - Storage system for Java + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Storage + Copyright 2023 + Dorkbox LLC - - Java Uuid Generator - A set of Java classes for working with UUIDs - [The Apache Software License, Version 2.0] - https://github.com/cowtowncoder/java-uuid-generator - Copyright 2022 - Tatu Saloranta (tatu.saloranta@iki.fi) - Contributors. See source release-notes/CREDITS + Extra license information + - Kryo - Fast and efficient binary object graph serialization framework for Java + [BSD 3-Clause License] + https://github.com/EsotericSoftware/kryo + Copyright 2023 + Nathan Sweet - - kotlin-logging - Lightweight logging framework for Kotlin - [The Apache Software License, Version 2.0] - https://github.com/MicroUtils/kotlin-logging - Copyright 2022 - Ohad Shai + Extra license information + - ReflectASM - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/reflectasm + Nathan Sweet - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch + - Objenesis - + [The Apache Software License, Version 2.0] + https://github.com/easymock/objenesis + Objenesis Team and all contributors - - XZ for Java - Complete implementation of XZ data compression in pure Java - [Public Domain, per Creative Commons CC0] - https://tukaani.org/xz/java.html - Copyright 2022 - Lasse Collin - Igor Pavlov + - MinLog-SLF4J - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/minlog + Nathan Sweet - Kotlin - [The Apache Software License, Version 2.0] @@ -1851,119 +1264,84 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - JNA - Simplified native library access for Java. - [The Apache Software License, Version 2.0] - https://github.com/twall/jna - Copyright 2022 - Timothy Wall - - - JNA-Platform - Mappings for a number of commonly used platform functions - [The Apache Software License, Version 2.0] - https://github.com/twall/jna - Copyright 2022 - Timothy Wall - - - Netty - An event-driven asynchronous network application framework - [The Apache Software License, Version 2.0] - https://netty.io - Copyright 2022 - The Netty Project - Contributors. See source NOTICE - - - Bouncy Castle Crypto - Lightweight cryptography API and JCE Extension - [The Apache Software License, Version 2.0] - http://www.bouncycastle.org - Copyright 2022 - The Legion of the Bouncy Castle Inc - - - Lightweight Java Game Library - Java library that enables cross-platform access to popular native APIs - [BSD 3-Clause License] - https://github.com/LWJGL/lwjgl3 - Copyright 2022 - Lightweight Java Game Library - - - TypeTools - A simple, zero-dependency library for working with types. Supports Java 1.6+ and Android. - [The Apache Software License, Version 2.0] - https://github.com/jhalterman/typetools - Copyright 2022 - Jonathan Halterman and friends + - SLF4J - Simple facade or abstraction for various logging frameworks + [MIT License] + https://www.slf4j.org + Copyright 2023 + QOS.ch - - Collections - Niche collections to augment what is already available. + - ByteUtilities - Byte manipulation and SHA/xxHash utilities [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - Copyright 2022 + https://git.dorkbox.com/dorkbox/ByteUtilities + Copyright 2023 Dorkbox LLC Extra license information - - AhoCorasickDoubleArrayTrie - Niche collections to augment what is already available. + - Kryo Serialization - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/kryo + Copyright 2020 + Nathan Sweet + + - Base58 - [The Apache Software License, Version 2.0] - https://github.com/hankcs/AhoCorasickDoubleArrayTrie + https://bitcoinj.github.io + https://github.com/komputing/KBase58 Copyright 2018 - hankcs - - - Bias, BinarySearch - - [MIT License] - https://git.dorkbox.com/dorkbox/Collections - https://github.com/timboudreau/util - Copyright 2013 - Tim Boudreau + Google Inc + Andreas Schildbach + ligi - - ConcurrentEntry - + - Kotlin - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - Copyright 2016 - bennidi - dorkbox + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Collection Utilities (Array, ArrayMap, BooleanArray, ByteArray, CharArray, FloatArray, IdentityMap, IntArray, IntFloatMap, IntIntMap, IntMap, IntSet, LongArray, LongMap, ObjectFloatMap, ObjectIntMap, ObjectMap, ObjectSet, OrderedMap, OrderedSet) - + - Netty - An event-driven asynchronous network application framework [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils - Copyright 2011 - LibGDX - Mario Zechner (badlogicgames@gmail.com) - Nathan Sweet (nathan.sweet@gmail.com) + https://netty.io + Copyright 2023 + The Netty Project + Contributors. See source NOTICE - - Predicate - - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils - Copyright 2011 - LibGDX - Mario Zechner (badlogicgames@gmail.com) - Nathan Sweet (nathan.sweet@gmail.com) - xoppa + - Kryo - Fast and efficient binary object graph serialization framework for Java + [BSD 3-Clause License] + https://github.com/EsotericSoftware/kryo + Copyright 2023 + Nathan Sweet - - Select, QuickSelect - - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils - Copyright 2011 - LibGDX - Mario Zechner (badlogicgames@gmail.com) - Nathan Sweet (nathan.sweet@gmail.com) - Jon Renner + Extra license information + - ReflectASM - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/reflectasm + Nathan Sweet - - TimSort, ComparableTimSort - - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Collections - https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils - Copyright 2008 - The Android Open Source Project + - Objenesis - + [The Apache Software License, Version 2.0] + https://github.com/easymock/objenesis + Objenesis Team and all contributors - - ConcurrentWeakIdentityHashMap - Concurrent WeakIdentity HashMap - [The Apache Software License, Version 2.0] - https://github.com/spring-projects/spring-loaded/blob/master/springloaded/src/main/java/org/springsource/loaded/support/ConcurrentWeakIdentityHashMap.java - Copyright 2016 - zhanhb + - MinLog-SLF4J - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/minlog + Nathan Sweet - - Kotlin - + - LZ4 and xxHash - LZ4 compression for Java, based on Yann Collet's work [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + https://github.com/lz4/lz4 + Copyright 2023 + Yann Collet + Adrien Grand + + - XZ for Java - Complete implementation of XZ data compression in pure Java + [Public Domain, per Creative Commons CC0] + https://tukaani.org/xz/java.html + Copyright 2023 + Lasse Collin + Igor Pavlov - Updates - Software Update Management [The Apache Software License, Version 2.0] @@ -1980,24 +1358,19 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Executor - Shell, JVM, and SSH command execution on Linux, MacOS, or Windows for Java 8+ + - Json - Lightweight Kotlin/JSON serialization [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Executor - Copyright 2022 + https://git.dorkbox.com/dorkbox/Json + Copyright 2023 Dorkbox LLC Extra license information - - ZT Process Executor - - [The Apache Software License, Version 2.0] - https://github.com/zeroturnaround/zt-exec - Copyright 2014 - ZeroTurnaround LLC - - - Apache Commons Exec - + - LibGDX Json Utils - Java object graphs, to and from JSON automatically [The Apache Software License, Version 2.0] - https://commons.apache.org/proper/commons-exec/ - Copyright 2014 - The Apache Software Foundation + https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils + Copyright 2011 + Mario Zechner + Nathan Sweet - Kotlin - [The Apache Software License, Version 2.0] @@ -2007,57 +1380,115 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support - [The Apache Software License, Version 2.0] - https://github.com/Kotlin/kotlinx.coroutines - Copyright 2022 - JetBrains s.r.o. - - SLF4J - Simple facade or abstraction for various logging frameworks [MIT License] - http://www.slf4j.org - Copyright 2022 + https://www.slf4j.org + Copyright 2023 QOS.ch - - Logback - Logback is a logging framework for Java applications + - Updates - Software Update Management [The Apache Software License, Version 2.0] - http://logback.qos.ch - Copyright 2022 - QOS.ch + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 + Dorkbox LLC - - SSHJ - SSHv2 library for Java + Extra license information + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - Collections - Collection types and utilities to enhance the default collections. [The Apache Software License, Version 2.0] - https://github.com/hierynomus/sshj - Copyright 2022 - Jeroen van Erp - SSHJ Contributors + https://git.dorkbox.com/dorkbox/Collections + Copyright 2023 + Dorkbox LLC Extra license information - - Apache MINA - + - Bias, BinarySearch - + [MIT License] + https://git.dorkbox.com/dorkbox/Collections + https://github.com/timboudreau/util + Copyright 2013 + Tim Boudreau + + - ConcurrentEntry - [The Apache Software License, Version 2.0] - https://mina.apache.org/sshd-project/ - The Apache Software Foundation + https://git.dorkbox.com/dorkbox/Collections + Copyright 2016 + bennidi + dorkbox - - Apache Commons-Net - + - Collection Utilities (Array, ArrayMap, BooleanArray, ByteArray, CharArray, FloatArray, IdentityMap, IntArray, IntFloatMap, IntIntMap, IntMap, IntSet, LongArray, LongMap, ObjectFloatMap, ObjectIntMap, ObjectMap, ObjectSet, OrderedMap, OrderedSet) - [The Apache Software License, Version 2.0] - https://commons.apache.org/proper/commons-net/ - The Apache Software Foundation + https://git.dorkbox.com/dorkbox/Collections + https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils + Copyright 2011 + LibGDX + Mario Zechner (badlogicgames@gmail.com) + Nathan Sweet (nathan.sweet@gmail.com) - - JZlib - + - Predicate - [The Apache Software License, Version 2.0] - http://www.jcraft.com/jzlib - Atsuhiko Yamanaka - JCraft, Inc. + https://git.dorkbox.com/dorkbox/Collections + https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils + Copyright 2011 + LibGDX + Mario Zechner (badlogicgames@gmail.com) + Nathan Sweet (nathan.sweet@gmail.com) + xoppa - - Bouncy Castle Crypto - + - Select, QuickSelect - [The Apache Software License, Version 2.0] - http://www.bouncycastle.org - The Legion of the Bouncy Castle Inc + https://git.dorkbox.com/dorkbox/Collections + https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils + Copyright 2011 + LibGDX + Mario Zechner (badlogicgames@gmail.com) + Nathan Sweet (nathan.sweet@gmail.com) + Jon Renner + + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - Updates - Software Update Management + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 + Dorkbox LLC + + Extra license information + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - ed25519-java - - [Public Domain, per Creative Commons CC0] - https://github.com/str4d/ed25519-java - https://github.com/str4d + - MinLog - Drop-in replacement for MinLog to log through SLF4j. + [BSD 3-Clause License] + https://git.dorkbox.com/dorkbox/MinLog-SLF4J + https://github.com/EsotericSoftware/minlog + Copyright 2023 + Dorkbox LLC + Nathan Sweet + Dan Brown + + Extra license information + - SLF4J - Simple facade or abstraction for various logging frameworks + [MIT License] + https://www.slf4j.org + Copyright 2023 + QOS.ch - Updates - Software Update Management [The Apache Software License, Version 2.0] @@ -2074,51 +1505,24 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - NetworkUtils - Utilities for managing network configurations, IP/MAC address conversion, and ping (via OS native commands) + - ObjectPool - Fast, lightweight, and compatible blocking/non-blocking/soft-reference object pool for Java 8+ [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/NetworkUtils - Copyright 2022 + https://git.dorkbox.com/dorkbox/ObjectPool + Copyright 2023 Dorkbox LLC Extra license information - - Netty - - [The Apache Software License, Version 2.0] - https://netty.io/ - Copyright 2014 - The Netty Project - This product contains a modified portion of Netty Network Utils - - - Apache Harmony - - [The Apache Software License, Version 2.0] - http://archive.apache.org/dist/harmony/ - Copyright 2010 - The Apache Software Foundation - This product contains a modified portion of 'Apache Harmony', an open source Java SE - - - Apache HTTP Utils - - [The Apache Software License, Version 2.0] - http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/psl/ - Copyright 2010 - The Apache Software Foundation - This product contains a modified portion of 'PublicSuffixDomainFilter.java' - - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch - - - JNA - Simplified native library access for Java. + - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support [The Apache Software License, Version 2.0] - https://github.com/twall/jna - Copyright 2022 - Timothy Wall + https://github.com/Kotlin/kotlinx.coroutines + Copyright 2023 + JetBrains s.r.o. - - JNA-Platform - Mappings for a number of commonly used platform functions + - Conversant Disruptor - Disruptor is the highest performing intra-thread transfer mechanism available in Java. [The Apache Software License, Version 2.0] - https://github.com/twall/jna - Copyright 2022 - Timothy Wall + https://github.com/conversant/disruptor + Copyright 2023 + Conversant, Inc - Kotlin - [The Apache Software License, Version 2.0] @@ -2128,25 +1532,13 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Executor - Shell, JVM, and SSH command execution on Linux, MacOS, or Windows for Java 8+ + - Updates - Software Update Management [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Executor - Copyright 2022 + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 Dorkbox LLC Extra license information - - ZT Process Executor - - [The Apache Software License, Version 2.0] - https://github.com/zeroturnaround/zt-exec - Copyright 2014 - ZeroTurnaround LLC - - - Apache Commons Exec - - [The Apache Software License, Version 2.0] - https://commons.apache.org/proper/commons-exec/ - Copyright 2014 - The Apache Software Foundation - - Kotlin - [The Apache Software License, Version 2.0] https://github.com/JetBrains/kotlin @@ -2155,72 +1547,55 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support - [The Apache Software License, Version 2.0] - https://github.com/Kotlin/kotlinx.coroutines - Copyright 2022 - JetBrains s.r.o. - - - SLF4J - Simple facade or abstraction for various logging frameworks - [MIT License] - http://www.slf4j.org - Copyright 2022 - QOS.ch - - - Logback - Logback is a logging framework for Java applications - [The Apache Software License, Version 2.0] - http://logback.qos.ch - Copyright 2022 - QOS.ch - - - SSHJ - SSHv2 library for Java - [The Apache Software License, Version 2.0] - https://github.com/hierynomus/sshj - Copyright 2022 - Jeroen van Erp - SSHJ Contributors - - Extra license information - - Apache MINA - - [The Apache Software License, Version 2.0] - https://mina.apache.org/sshd-project/ - The Apache Software Foundation + - Serializers - Kryo based serializers + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Serializers + Copyright 2023 + Dorkbox LLC - - Apache Commons-Net - - [The Apache Software License, Version 2.0] - https://commons.apache.org/proper/commons-net/ - The Apache Software Foundation + Extra license information + - Kryo Serializers - + [The Apache Software License, Version 2.0] + https://github.com/magro/kryo-serializers + Copyright 2021 + Martin Grotzke + Rafael Winterhalter - - JZlib - - [The Apache Software License, Version 2.0] - http://www.jcraft.com/jzlib - Atsuhiko Yamanaka - JCraft, Inc. + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md - - Bouncy Castle Crypto - - [The Apache Software License, Version 2.0] - http://www.bouncycastle.org - The Legion of the Bouncy Castle Inc + - Kryo - Fast and efficient binary object graph serialization framework for Java + [BSD 3-Clause License] + https://github.com/EsotericSoftware/kryo + Copyright 2023 + Nathan Sweet - - ed25519-java - - [Public Domain, per Creative Commons CC0] - https://github.com/str4d/ed25519-java - https://github.com/str4d + Extra license information + - ReflectASM - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/reflectasm + Nathan Sweet - - Updates - Software Update Management + - Objenesis - [The Apache Software License, Version 2.0] - https://git.dorkbox.com/dorkbox/Updates - Copyright 2021 - Dorkbox LLC + https://github.com/easymock/objenesis + Objenesis Team and all contributors - Extra license information - - Kotlin - - [The Apache Software License, Version 2.0] - https://github.com/JetBrains/kotlin - Copyright 2020 - JetBrains s.r.o. and Kotlin Programming Language contributors - Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply - See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + - MinLog-SLF4J - + [BSD 3-Clause License] + https://github.com/EsotericSoftware/minlog + Nathan Sweet + + - Bouncy Castle Crypto - Lightweight cryptography API and JCE Extension + [The Apache Software License, Version 2.0] + https://www.bouncycastle.org + Copyright 2023 + The Legion of the Bouncy Castle Inc - Updates - Software Update Management [The Apache Software License, Version 2.0] @@ -2237,10 +1612,110 @@ Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + - Updates - Software Update Management + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 + Dorkbox LLC + + Extra license information + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - Updates - Software Update Management + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Updates + Copyright 2021 + Dorkbox LLC + + Extra license information + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - Utilities - Utilities for use within Java projects + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Utilities + Copyright 2023 + Dorkbox LLC + + Extra license information + - MersenneTwisterFast - + [BSD 3-Clause License] + https://git.dorkbox.com/dorkbox/Utilities + Copyright 2003 + Sean Luke + Michael Lecuyer (portions Copyright 1993 + + - FastThreadLocal - + [BSD 3-Clause License] + https://git.dorkbox.com/dorkbox/Utilities + https://github.com/LWJGL/lwjgl3/blob/5819c9123222f6ce51f208e022cb907091dd8023/modules/core/src/main/java/org/lwjgl/system/FastThreadLocal.java + https://github.com/riven8192/LibStruct/blob/master/src/net/indiespot/struct/runtime/FastThreadLocal.java + Copyright 2014 + Lightweight Java Game Library Project + Riven + + - Retrofit - A type-safe HTTP client for Android and Java + [The Apache Software License, Version 2.0] + https://github.com/square/retrofit + Copyright 2020 + Square, Inc + + - Resource Listing - Listing the contents of a resource directory + [The Apache Software License, Version 2.0] + https://www.uofr.net/~greg/java/get-resource-listing.html + Copyright 2017 + Greg Briggs + + - CommonUtils - Common utility extension functions for kotlin + [The Apache Software License, Version 2.0] + https://www.pronghorn.tech + Copyright 2017 + Pronghorn Technology LLC + Dorkbox LLC + + - Kotlin Coroutine CountDownLatch - + [The Apache Software License, Version 2.0] + https://github.com/Kotlin/kotlinx.coroutines/issues/59 + https://github.com/venkatperi/kotlin-coroutines-lib + Copyright 2018 + Venkat Peri + + - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support + [The Apache Software License, Version 2.0] + https://github.com/Kotlin/kotlinx.coroutines + Copyright 2023 + JetBrains s.r.o. + + - Java Uuid Generator - A set of Java classes for working with UUIDs + [The Apache Software License, Version 2.0] + https://github.com/cowtowncoder/java-uuid-generator + Copyright 2023 + Tatu Saloranta (tatu.saloranta@iki.fi) + Contributors. See source release-notes/CREDITS + + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + - OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment. [The Apache Software License, Version 2.0] https://git.dorkbox.com/dorkbox/OS - Copyright 2022 + Copyright 2023 Dorkbox LLC Extra license information diff --git a/LICENSE.MPLv2 b/LICENSE.MPLv2 new file mode 100644 index 00000000..14e2f777 --- /dev/null +++ b/LICENSE.MPLv2 @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +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. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md index c8769be1..ea915304 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,66 @@ Network ======= -###### [![Dorkbox](https://badge.dorkbox.com/dorkbox.svg "Dorkbox")](https://git.dorkbox.com/dorkbox/Network) [![Github](https://badge.dorkbox.com/github.svg "Github")](https://github.com/dorkbox/Network) [![Gitlab](https://badge.dorkbox.com/gitlab.svg "Gitlab")](https://gitlab.com/dorkbox/Network) [![Bitbucket](https://badge.dorkbox.com/bitbucket.svg "Bitbucket")](https://bitbucket.org/dorkbox/Network) +###### [![Dorkbox](https://badge.dorkbox.com/dorkbox.svg "Dorkbox")](https://git.dorkbox.com/dorkbox/Network) [![Github](https://badge.dorkbox.com/github.svg "Github")](https://github.com/dorkbox/Network) [![Gitlab](https://badge.dorkbox.com/gitlab.svg "Gitlab")](https://gitlab.com/dorkbox/Network) -The Network project is an encrypted, high-performance, event-driven/reactive Network stack with DNS and RMI, using Netty, Kryo, KryoNet RMI, and LZ4 via TCP/UDP. +The Network project is an ~~encrypted~~, high-performance, event-driven/reactive Network stack with DNS and RMI, using Aeron, Kryo, KryoNet RMI, ~~encryption and LZ4 via UDP.~~ These are the main features: -* The connection between endpoints is AES256-GCM / EC curve25519. (WIP, this was updated for use with Aeron, which changes this) -* The connection data is LZ4 compressed and byte-packed for small payload sizes. (WIP, this was updated for use with Aeron, which - changes this) -- The connection supports: + +~~* The connection between endpoints is AES256-GCM / EC curve25519. (WIP, this was updated for use with Aeron, which changes this)~~ +~~* The connection data is LZ4 compressed and byte-packed for small payload sizes. (WIP, this was updated for use with Aeron, which + changes this)~~ +### The connection supports: + - Sending object (via the Kryo serialization framework) + - Sending arbitrarily large objects - Remote Method Invocation - Blocking - Non-Blocking - Void returns - Exceptions can be returned - Kotlin coroutine suspend functions - - Sending data when Idle + - ~~Sending data when Idle~~ - "Pinging" the remote end (for measuring round-trip time) - Firewall connections by IP+CIDR - - Specify the connection type (nothing, compress, compress+encrypt) + - ~~Specify the connection type (nothing, compress, compress+encrypt)~~ +- The available transports is UDP -- The available transports are TCP and UDP -- There are simple wrapper classes for: - - Server - - Client - * MultiCast Broadcast client and server discovery (WIP, this was updated for use with Aeron, which changes this) - - -- Note: There is a maximum packet size for UDP, 508 bytes *to guarantee it's unfragmented* - -- This is for cross-platform use, specifically - linux 32/64, mac 64, and windows 32/64. Java 1.8+ +- This is for cross-platform use, specifically - linux 32/64, mac 64, and windows 32/64. Java 1.11+ +- This library is designed to be used with kotlin, specifically the use of coroutines. ``` java -public static -class AMessage { - public - AMessage() { - } +val configurationServer = ServerConfiguration() +configurationServer.settingsStore = Storage.Memory() // don't want to persist anything on disk! +configurationServer.port = 2000 +configurationServer.enableIPv4 = true + +val server: Server = Server(configurationServer) + +server.onMessage { message -> + logger.error("Received message '$message'") } -KryoCryptoSerializationManager.DEFAULT.register(AMessage.class); - -Configuration configuration = new Configuration(); -configuration.tcpPort = tcpPort; -configuration.host = host; - -final Server server = new Server(configuration); -addEndPoint(server); -server.bind(false); - -server.listeners() - .add(new Listener() { - @Override - public - void received(Connection connection, AMessage object) { - System.err.println("Server received message from client. Bouncing back."); - connection.send() - .TCP(object); - } - }); - -Client client = new Client(configuration); -client.disableRemoteKeyValidation(); -addEndPoint(client); -client.connect(5000); - -client.listeners() - .add(new Listener() { - @Override - public - void received(Connection connection, AMessage object) { - ClientSendTest.this.checkPassed.set(true); - System.err.println("Tada! It's been bounced back."); - server.stop(); - } - }); - -client.send() - .TCP(new AMessage()); +server.bind() + + + +val configurationClient = ClientConfiguration() +configurationClient.settingsStore = Storage.Memory() // don't want to persist anything on disk! +configurationClient.port = 2000 + +val client: Client = Client(configurationClient) + +client.onConnect { + send("client test message") +} + +client.connect() ``` +     @@ -95,7 +72,7 @@ Maven Info com.dorkbox Network - 5.32 + 6.15 ``` @@ -105,11 +82,11 @@ Gradle Info ``` dependencies { ... - implementation("com.dorkbox:Network:5.32") + implementation("com.dorkbox:Network:6.15") } ``` License --------- -This project is © 2021 dorkbox llc, and is distributed under the terms of the Apache v2.0 License. See file "LICENSE" for further +This project is © 2023 dorkbox llc, and is distributed under the terms of the Apache v2.0 License. See file "LICENSE" for further references. diff --git a/build.gradle.kts b/build.gradle.kts index bd311418..80ea51f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,6 @@ * limitations under the License. */ -import java.time.Instant - /////////////////////////////// ////// PUBLISH TO SONATYPE / MAVEN CENTRAL ////// TESTING : (to local maven repo) <'publish and release' - 'publishToMavenLocal'> @@ -25,19 +23,22 @@ import java.time.Instant gradle.startParameter.showStacktrace = ShowStacktrace.ALWAYS // always show the stacktrace! plugins { - id("com.dorkbox.GradleUtils") version "2.17" - id("com.dorkbox.Licensing") version "2.12" - id("com.dorkbox.VersionUpdate") version "2.5" - id("com.dorkbox.GradlePublish") version "1.12" + id("com.dorkbox.GradleUtils") version "3.18" + id("com.dorkbox.Licensing") version "2.28" + id("com.dorkbox.VersionUpdate") version "2.8" + id("com.dorkbox.GradlePublish") version "1.22" + + id("com.github.johnrengelman.shadow") version "8.1.1" - kotlin("jvm") version "1.6.10" + kotlin("jvm") version "1.9.0" } +@Suppress("ConstPropertyName") object Extras { // set for the project const val description = "High-performance, event-driven/reactive network stack for Java 11+" const val group = "com.dorkbox" - const val version = "5.32" + const val version = "6.15" // set as project.ext const val name = "Network" @@ -45,8 +46,6 @@ object Extras { const val vendor = "Dorkbox LLC" const val vendorUrl = "https://dorkbox.com" const val url = "https://git.dorkbox.com/dorkbox/Network" - - val buildDate = Instant.now().toString() } /////////////////////////////// @@ -60,31 +59,38 @@ GradleUtils.compileConfiguration(JavaVersion.VERSION_11) { // enable the use of inline classes. see https://kotlinlang.org/docs/reference/inline-classes.html freeCompilerArgs = listOf("-Xinline-classes") } -//NOTE: we do not support JPMS yet, as there are some libraries missing support for it still -// ratelimiter, "other" package +//val kotlin = project.extensions.getByType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension::class.java).sourceSets.getByName("main").kotlin +//kotlin.apply { +// setSrcDirs(project.files("src")) +// include("**/*.kt") // want to include kotlin files for the source. 'setSrcDirs' resets includes... +//} + +// TODO: driver name resolution: https://github.com/real-logic/aeron/wiki/Driver-Name-Resolution +// this keeps us from having to restart the media driver when a connection changes IP addresses + +// TODO: virtual threads in java21 for polling? + +// if we are sending a SMALL byte array, then we SEND IT DIRECTLY in a more optimized manner (because we can count size info!) +// other side has to be able to parse/know that this was sent directly as bytes. It could be game state data, or voice data, etc. +// another idea is to be able to "send" a stream of bytes (this would also get chunked/etc!). if chunked, these are fixed byte sizes! +// -- the first byte manage: byte/message/stream/etc, no-crypt, crypt, crypt+compress +// - connection.inputStream() --> behaves as an input stream to remote endpoint --> connection.outputStream() +// -- open/close/flush/etc commands also go through +// -- this can be used to stream files/audio/etc VERY easily +// -- have a createInputStream(), which will cause the outputStream() on the remote end to be created. +// --- this remote outputStream is a file, raw??? this is setup by createInputStream() on the remote end +// - state-machine for kryo class registrations? + +// ratelimiter, "other" package, send-on-idle // rest of unit tests // getConnectionUpgradeType // ability to send with a function callback (using RMI waiter type stuff for callbacks) -// use conscrypt?! // java 14 is faster with aeron! -// NOTE: now using aeron instead of netty -// todo: remove BC! use conscrypt instead, or native java? (if possible. we are java 11 now, instead of 1.6) -// also, NOT using bouncastle, but instead the google one -// better SSL library -// implementation("org.conscrypt:conscrypt-openjdk-uber:2.2.1") -// init { -// try { -// Security.insertProviderAt(Conscrypt.newProvider(), 1); -// } -// catch (e: Throwable) { -// e.printStackTrace(); -// } -// } licensing { @@ -134,66 +140,92 @@ tasks.jar.get().apply { attributes["Specification-Vendor"] = Extras.vendor attributes["Implementation-Title"] = "${Extras.group}.${Extras.id}" - attributes["Implementation-Version"] = Extras.buildDate + attributes["Implementation-Version"] = GradleUtils.now() attributes["Implementation-Vendor"] = Extras.vendor } } + +val shadowJar: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar by tasks +shadowJar.apply { + manifest.inheritFrom(tasks.jar.get().manifest) + + manifest.attributes.apply { + put("Main-Class", "dorkboxTest.network.app.AeronClientServerForever") + } + + mergeServiceFiles() + + duplicatesStrategy = DuplicatesStrategy.INCLUDE + + from(sourceSets.test.get().output) + configurations = listOf(project.configurations.testRuntimeClasspath.get()) + + archiveBaseName.set(project.name + "-all") +} + + dependencies { - api("org.jetbrains.kotlinx:atomicfu:0.17.3") - api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2") + api("org.jetbrains.kotlinx:atomicfu:0.23.0") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") // https://github.com/dorkbox - api("com.dorkbox:ByteUtilities:1.5") - api("com.dorkbox:Collections:1.1") - api("com.dorkbox:MinLog:2.4") - api("com.dorkbox:NetworkDNS:2.7.1") - api("com.dorkbox:NetworkUtils:2.18") - api("com.dorkbox:ObjectPool:4.0") - api("com.dorkbox:OS:1.0") - api("com.dorkbox:Serializers:2.7") - api("com.dorkbox:Storage:1.1") + api("com.dorkbox:ByteUtilities:2.1") + api("com.dorkbox:ClassUtils:1.3") + api("com.dorkbox:Collections:2.7") + api("com.dorkbox:HexUtilities:1.1") + api("com.dorkbox:JNA:1.4") + api("com.dorkbox:MinLog:2.7") + api("com.dorkbox:NetworkDNS:2.16") + api("com.dorkbox:NetworkUtils:2.23") + api("com.dorkbox:OS:1.11") + api("com.dorkbox:Serializers:2.9") + api("com.dorkbox:Storage:1.11") api("com.dorkbox:Updates:1.1") - api("com.dorkbox:Utilities:1.29") + api("com.dorkbox:Utilities:1.48") + + + // how we bypass using reflection/jpms to access fields for java17+ + api("org.javassist:javassist:3.29.2-GA") + + + val jnaVersion = "5.13.0" + api("net.java.dev.jna:jna-jpms:${jnaVersion}") + api("net.java.dev.jna:jna-platform-jpms:${jnaVersion}") - // we include ALL of aeron, in case we need to debug aeron behavior // https://github.com/real-logic/aeron - val aeronVer = "1.38.1" - api("io.aeron:aeron-all:$aeronVer") -// api("io.aeron:aeron-client:$aeronVer") -// api("io.aeron:aeron-driver:$aeronVer") + val aeronVer = "1.42.1" + api("io.aeron:aeron-driver:$aeronVer") + // ALL of aeron, in case we need to debug aeron behavior +// api("io.aeron:aeron-all:$aeronVer") +// api("org.agrona:agrona:1.18.2") // sources for this aren't included in aeron-all! // https://github.com/EsotericSoftware/kryo - api("com.esotericsoftware:kryo:5.3.0") { + api("com.esotericsoftware:kryo:5.5.0") { exclude("com.esotericsoftware", "minlog") // we use our own minlog, that logs to SLF4j instead } - // https://github.com/jpountz/lz4-java -// implementation("net.jpountz.lz4:lz4:1.3.0") - // this is NOT the same thing as LMAX disruptor. - // This is just a slightly faster queue than LMAX. (LMAX is a fast queue + other things w/ a difficult DSL) - // https://github.com/conversant/disruptor_benchmark - // https://www.youtube.com/watch?v=jVMOgQgYzWU - //api("com.conversantmedia:disruptor:1.2.19") + // https://github.com/lz4/lz4-java + api("org.lz4:lz4-java:1.8.0") - // https://github.com/jhalterman/typetools - api("net.jodah:typetools:0.6.3") // Expiring Map (A high performance thread-safe map that expires entries) // https://github.com/jhalterman/expiringmap - api("net.jodah:expiringmap:0.5.10") + api("net.jodah:expiringmap:0.5.11") // https://github.com/MicroUtils/kotlin-logging - api("io.github.microutils:kotlin-logging:2.1.23") - api("org.slf4j:slf4j-api:1.8.0-beta4") - +// api("io.github.microutils:kotlin-logging:3.0.5") + implementation("org.slf4j:slf4j-api:2.0.9") testImplementation("junit:junit:4.13.2") - testImplementation("ch.qos.logback:logback-classic:1.3.0-alpha4") + testImplementation("ch.qos.logback:logback-classic:1.4.5") + testImplementation("io.aeron:aeron-all:$aeronVer") + + testImplementation("com.dorkbox:Config:2.1") } publishToSonatype { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f..7f93135c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b1159fc5..8da735bc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..0adc8e1a 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${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"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ 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. + if ! command -v java >/dev/null 2>&1 + then + 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 fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,6 +198,10 @@ if "$cygwin" || "$msys" ; then done fi + +# 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"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f9..6689b85b 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 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 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/not-fixed/CryptoEccNative.kt b/not-fixed/CryptoEccNative.kt deleted file mode 100644 index 23fceb97..00000000 --- a/not-fixed/CryptoEccNative.kt +++ /dev/null @@ -1,293 +0,0 @@ -package dorkbox.network.other - -import java.math.BigInteger -import java.security.GeneralSecurityException -import java.security.KeyFactory -import java.security.KeyPair -import java.security.KeyPairGenerator -import java.security.PrivateKey -import java.security.SecureRandom -import java.security.interfaces.ECPrivateKey -import java.security.interfaces.ECPublicKey -import java.security.spec.ECField -import java.security.spec.ECFieldFp -import java.security.spec.ECParameterSpec -import java.security.spec.ECPoint -import java.security.spec.ECPublicKeySpec -import java.security.spec.EllipticCurve -import java.security.spec.NamedParameterSpec -import java.security.spec.PKCS8EncodedKeySpec -import java.security.spec.X509EncodedKeySpec -import javax.crypto.Cipher - - - -/** - * - */ -private object CryptoEccNative { - // see: https://openjdk.java.net/jeps/324 - - const val curve25519 = "curve25519" - const val default_curve = curve25519 - - const val macSize = 512 - // on NIST vs 25519 vs Brainpool, see: - // - http://ogryb.blogspot.de/2014/11/why-i-dont-trust-nist-p-256.html - // - http://credelius.com/credelius/?p=97 - // - http://safecurves.cr.yp.to/ - // we should be using 25519, because NIST and brainpool are "unsafe". Brainpool is "more random" than 25519, but is still not considered safe. - - // more info about ECC from: - // http://www.johannes-bauer.com/compsci/ecc/?menuid=4 - // http://stackoverflow.com/questions/7419183/problems-implementing-ecdh-on-android-using-bouncycastle - // http://tools.ietf.org/html/draft-jivsov-openpgp-ecc-06#page-4 - // http://www.nsa.gov/ia/programs/suiteb_cryptography/ - // https://github.com/nelenkov/ecdh-kx/blob/master/src/org/nick/ecdhkx/Crypto.java - // http://nelenkov.blogspot.com/2011/12/using-ecdh-on-android.html - // http://www.secg.org/collateral/sec1_final.pdf - - // More info about 25519 key types (ed25519 and X25519) - // https://blog.filippo.io/using-ed25519-keys-for-encryption/ - - - fun createKeyPair(secureRandom: SecureRandom): KeyPair { - val kpg: KeyPairGenerator = KeyPairGenerator.getInstance("XDH") - kpg.initialize(NamedParameterSpec.X25519, secureRandom) - return kpg.generateKeyPair() - - -// println("--- Public Key ---") -// val publicKey = kp.public -// -// System.out.println(publicKey.algorithm) // XDH -// System.out.println(publicKey.format) // X.509 -// -// // save this public key -// val pubKey = publicKey.encoded -// -// println("---") -// -// println("--- Private Key ---") -// val privateKey = kp.private -// -// System.out.println(privateKey.algorithm); // XDH -// System.out.println(privateKey.format); // PKCS#8 -// -// // save this private key -// val priKey = privateKey.encoded - - -// val kf: KeyFactory = KeyFactory.getInstance("XDH"); - -// //BigInteger u = ... -// val pubSpec: XECPublicKeySpec = XECPublicKeySpec(paramSpec, u); -// val pubKey: PublicKey = kf.generatePublic(pubSpec); -// // -// -// val ka: KeyAgreement = KeyAgreement.getInstance("XDH"); -// ka.init(kp.private); - //ka.doPhase(pubKey, true); - //byte[] secret = ka.generateSecret(); - } - - - - private val FieldP_2: BigInteger = BigInteger.TWO // constant for scalar operations - private val FieldP_3: BigInteger = BigInteger.valueOf(3) // constant for scalar operations - private const val byteVal1 = 1.toByte() - - @Throws(GeneralSecurityException::class) - fun getPublicKey(pk: ECPrivateKey): ECPublicKey? { - val params: ECParameterSpec = pk.params - val w: ECPoint = scalmultNew(params, params.generator, pk.s) - - //final ECPoint w = scalmult(params.getCurve(), pk.getParams().getGenerator(), pk.getS()); - val kg: KeyFactory = KeyFactory.getInstance("EC") - return kg.generatePublic(ECPublicKeySpec(w, params)) as ECPublicKey - } - - private fun scalmultNew(params: ECParameterSpec, g: ECPoint, kin: BigInteger): ECPoint { - val curve = params.curve - val field = curve.field - if (field !is ECFieldFp) throw java.lang.UnsupportedOperationException(field::class.java.canonicalName) - - val p = field.p - val a = curve.a - var R = ECPoint.POINT_INFINITY - - // value only valid for curve secp256k1, code taken from https://www.secg.org/sec2-v2.pdf, - // see "Finally the order n of G and the cofactor are: n = "FF.." - val SECP256K1_Q = params.order - //BigInteger SECP256K1_Q = new BigInteger("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141",16); - var k = kin.mod(SECP256K1_Q) // uses this ! - // BigInteger k = kin.mod(p); // do not use this ! wrong as per comment from President James Moveon Polk - val length = k.bitLength() - val binarray = ByteArray(length) - - for (i in 0..length - 1) { - binarray[i] = k.mod(FieldP_2).byteValueExact() - k = k.shiftRight(1) - } - for (i in length - 1 downTo 0) { - R = doublePoint(p, a, R) - if (binarray[i] == byteVal1) R = addPoint(p, a, R, g) - } - - return R - } - - fun scalmultOrg(curve: EllipticCurve, g: ECPoint, kin: BigInteger): ECPoint { - val field: ECField = curve.getField() - if (field !is ECFieldFp) throw UnsupportedOperationException(field::class.java.canonicalName) - val p: BigInteger = (field as ECFieldFp).getP() - val a: BigInteger = curve.getA() - var R = ECPoint.POINT_INFINITY - // value only valid for curve secp256k1, code taken from https://www.secg.org/sec2-v2.pdf, - // see "Finally the order n of G and the cofactor are: n = "FF.." - val SECP256K1_Q = BigInteger("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16) - var k = kin.mod(SECP256K1_Q) // uses this ! - - // wrong as per comment from President James Moveon Polk - // BigInteger k = kin.mod(p); // do not use this ! - println(" SECP256K1_Q: $SECP256K1_Q") - println(" p: $p") - - System.out.println("curve: " + curve.toString()) - val length = k.bitLength() - val binarray = ByteArray(length) - for (i in 0..length - 1) { - binarray[i] = k.mod(FieldP_2).byteValueExact() - k = k.shiftRight(1) - } - for (i in length - 1 downTo 0) { - R = doublePoint(p, a, R) - if (binarray[i] == byteVal1) R = addPoint(p, a, R, g) - } - return R - } - - // scalar operations for native java - // https://stackoverflow.com/a/42797410/8166854 - // written by author: SkateScout - private fun doublePoint(p: BigInteger, a: BigInteger, R: ECPoint): ECPoint? { - if (R == ECPoint.POINT_INFINITY) return R - - var slope = R.affineX.pow(2).multiply(FieldP_3) - slope = slope.add(a) - slope = slope.multiply(R.affineY.multiply(FieldP_2).modInverse(p)) - - val Xout = slope.pow(2).subtract(R.affineX.multiply(FieldP_2)).mod(p) - val Yout = R.affineY.negate().add(slope.multiply(R.affineX.subtract(Xout))).mod(p) - - return ECPoint(Xout, Yout) - } - - private fun addPoint(p: BigInteger, a: BigInteger, r: ECPoint, g: ECPoint): ECPoint? { - if (r == ECPoint.POINT_INFINITY) return g - if (g == ECPoint.POINT_INFINITY) return r - - if (r == g || r == g) return doublePoint(p, a, r) - - val gX = g.affineX - val sY = g.affineY - val rX = r.affineX - val rY = r.affineY - - val slope = rY.subtract(sY).multiply(rX.subtract(gX).modInverse(p)).mod(p) - val Xout = slope.modPow(FieldP_2, p).subtract(rX).subtract(gX).mod(p) - var Yout = sY.negate().mod(p) - Yout = Yout.add(slope.multiply(gX.subtract(Xout))).mod(p) - return ECPoint(Xout, Yout) - } - - - private fun byteArrayToHexString(a: ByteArray): String { - val sb = StringBuilder(a.size * 2) - for (b in a) sb.append(String.format("%02X", b)) - return sb.toString() - } - - fun hexStringToByteArray(s: String): ByteArray { - val len = s.length - val data = ByteArray(len / 2) - var i = 0 - while (i < len) { - data[i / 2] = ((Character.digit(s[i], 16) shl 4) - + Character.digit(s[i + 1], 16)).toByte() - i += 2 - } - return data - } - - @Throws(GeneralSecurityException::class) - @JvmStatic - fun main(args: Array) { - val cryptoText = "i23j4jh234kjh234kjh23lkjnfa9s8egfuypuh325" - - // NOTE: THIS IS NOT 25519!! - println("Generate ECPublicKey from PrivateKey (String) for curve secp256k1 (final)") - println("Check keys with https://gobittest.appspot.com/Address") - - // https://gobittest.appspot.com/Address - val privateKey = "D12D2FACA9AD92828D89683778CB8DFCCDBD6C9E92F6AB7D6065E8AACC1FF6D6" - val publicKeyExpected = "04661BA57FED0D115222E30FE7E9509325EE30E7E284D3641E6FB5E67368C2DB185ADA8EFC5DC43AF6BF474A41ED6237573DC4ED693D49102C42FFC88510500799" - println("\nprivatekey given : $privateKey") - println("publicKeyExpected: $publicKeyExpected") - - -// // routine with bouncy castle -// println("\nGenerate PublicKey from PrivateKey with BouncyCastle") -// val spec: ECNamedCurveParameterSpec = ECNamedCurveTable.getParameterSpec("secp256k1") // this ec curve is used for bitcoin operations -// val pointQ: org.bouncycastle.math.ec.ECPoint = spec.getG().multiply(BigInteger(1, ch.qos.logback.core.encoder.ByteArrayUtil.hexStringToByteArray(privateKey))) -// val publickKeyByte = pointQ.getEncoded(false) -// val publicKeyBc: String = byteArrayToHexString(publickKeyByte) -// println("publicKeyExpected: $publicKeyExpected") -// println("publicKey BC : $publicKeyBc") -// println("publicKeys match : " + publicKeyBc.contentEquals(publicKeyExpected)) - - // regeneration of ECPublicKey with java native starts here - println("\nGenerate PublicKey from PrivateKey with Java native routines") - // the preset "303E.." only works for elliptic curve secp256k1 - // see answer by user dave_thompson_085 - // https://stackoverflow.com/questions/48832170/generate-ec-public-key-from-byte-array-private-key-in-native-java-7 - - val privateKeyFull = "303E020100301006072A8648CE3D020106052B8104000A042730250201010420" + privateKey - val privateKeyFullByte: ByteArray = hexStringToByteArray(privateKeyFull) - - - println("privateKey full : $privateKeyFull") - val keyFactory = KeyFactory.getInstance("EC") - val privateKeyNative: PrivateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(privateKeyFullByte)) - val ecPrivateKeyNative = privateKeyNative as ECPrivateKey - - val ecPublicKeyNative = getPublicKey(ecPrivateKeyNative) - val ecPublicKeyNativeByte = ecPublicKeyNative!!.encoded - - val testPubKey = keyFactory.generatePublic(X509EncodedKeySpec(ecPublicKeyNativeByte)) as ECPublicKey - val equal = ecPublicKeyNativeByte.contentEquals(testPubKey.encoded) - - val publicKeyNativeFull: String = byteArrayToHexString(ecPublicKeyNativeByte) - val publicKeyNativeHeader = publicKeyNativeFull.substring(0, 46) - val publicKeyNativeKey = publicKeyNativeFull.substring(46, 176) - - println("ecPublicKeyFull : $publicKeyNativeFull") - println("ecPublicKeyHeader: $publicKeyNativeHeader") - println("ecPublicKeyKey : $publicKeyNativeKey") - println("publicKeyExpected: $publicKeyExpected") - println("publicKeys match : " + publicKeyNativeKey.contentEquals(publicKeyExpected)) - - - // encrypt - val encryptCipher: Cipher = Cipher.getInstance("RSA") - encryptCipher.init(Cipher.ENCRYPT_MODE, ecPublicKeyNative) - val cipherText: ByteArray = encryptCipher.doFinal(cryptoText.toByteArray()) - - // decrypt - val decryptCipher = Cipher.getInstance("RSA"); - decryptCipher.init(Cipher.DECRYPT_MODE, ecPrivateKeyNative); - - val outputBytes = decryptCipher.doFinal(cipherText) - println("Crypto round passed: ${String(outputBytes) == cryptoText}") - } -} diff --git a/not-fixed/LargeResizeBufferTest.java b/not-fixed/LargeResizeBufferTest.java deleted file mode 100755 index a6c1714c..00000000 --- a/not-fixed/LargeResizeBufferTest.java +++ /dev/null @@ -1,159 +0,0 @@ -/* Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.security.SecureRandom; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import dorkbox.network.connection.Connection; -import dorkbox.network.connection.Listener; -import dorkbox.network.serialization.Serialization; -import dorkbox.util.exceptions.SecurityException; -import dorkbox.util.serialization.SerializationManager; - -public -class LargeResizeBufferTest extends BaseTest { - private static final int OBJ_SIZE = 1024 * 100; - - private volatile int finalCheckAmount = 0; - private volatile int serverCheck = -1; - private volatile int clientCheck = -1; - - @Test - public - void manyLargeMessages() throws SecurityException, IOException { - final int messageCount = 1024; - - Configuration configuration = new Configuration(); - configuration.tcpPort = tcpPort; - configuration.udpPort = udpPort; - configuration.host = host; - register(configuration.serialization); - - Server server = new Server(configuration); - addEndPoint(server); - server.bind(false); - - server.listeners() - .add(new Listener.OnMessageReceived() { - AtomicInteger received = new AtomicInteger(); - AtomicInteger receivedBytes = new AtomicInteger(); - - @Override - public - void received(Connection connection, LargeMessage object) { - // System.err.println("Server ack message: " + received.get()); - - connection.send() - .TCP(object); - this.receivedBytes.addAndGet(object.bytes.length); - - if (this.received.incrementAndGet() == messageCount) { - System.out.println("Server received all " + messageCount + " messages!"); - System.out.println("Server received and sent " + this.receivedBytes.get() + " bytes."); - LargeResizeBufferTest.this.serverCheck = LargeResizeBufferTest.this.finalCheckAmount - this.receivedBytes.get(); - System.out.println("Server missed " + LargeResizeBufferTest.this.serverCheck + " bytes."); - stopEndPoints(); - } - } - }); - - Client client = new Client(configuration); - addEndPoint(client); - - client.listeners() - .add(new Listener.OnMessageReceived() { - AtomicInteger received = new AtomicInteger(); - AtomicInteger receivedBytes = new AtomicInteger(); - - @Override - public - void received(Connection connection, LargeMessage object) { - this.receivedBytes.addAndGet(object.bytes.length); - - int count = this.received.getAndIncrement(); - // System.out.println("Client received message: " + count); - - if (count == messageCount) { - System.out.println("Client received all " + messageCount + " messages!"); - System.out.println("Client received and sent " + this.receivedBytes.get() + " bytes."); - LargeResizeBufferTest.this.clientCheck = LargeResizeBufferTest.this.finalCheckAmount - this.receivedBytes.get(); - System.out.println("Client missed " + LargeResizeBufferTest.this.clientCheck + " bytes."); - } - } - }); - client.connect(5000); - - SecureRandom random = new SecureRandom(); - - System.err.println(" Client sending " + messageCount + " messages"); - for (int i = 0; i < messageCount; i++) { - this.finalCheckAmount += OBJ_SIZE; // keep increasing size - - byte[] b = new byte[OBJ_SIZE]; - random.nextBytes(b); - - // set some of the bytes to be all `244`, just so some compression can occur (to test that as well) - for (int j = 0; j < 400; j++) { - b[j] = (byte) 244; - } - -// System.err.println("Sending " + b.length + " bytes"); - client.send() - .TCP(new LargeMessage(b)); - } - - System.err.println("Client has queued " + messageCount + " messages."); - - waitForThreads(); - - if (this.clientCheck > 0) { - fail("Client missed " + this.clientCheck + " bytes."); - } - - if (this.serverCheck > 0) { - fail("Server missed " + this.serverCheck + " bytes."); - } - } - - private - void register(SerializationManager manager) { - manager.register(byte[].class); - manager.register(LargeMessage.class); - } - - public static - class LargeMessage { - public byte[] bytes; - - public - LargeMessage() { - } - - public - LargeMessage(byte[] bytes) { - this.bytes = bytes; - } - } -} diff --git a/not-fixed/Misc.kt b/not-fixed/Misc.kt deleted file mode 100755 index db43ab0e..00000000 --- a/not-fixed/Misc.kt +++ /dev/null @@ -1,197 +0,0 @@ -package dorkbox.network.other - -import kotlin.math.ceil - -/** - * - */ -object Misc { - - private fun annotations() { - // internal val classesWithRmiFields = IdentityMap, Array>() -// // get all classes that have fields with @Rmi field annotation. -// // THESE classes must be customized with our special RmiFieldSerializer serializer so that the @Rmi field is properly handled -// -// // SPECIFICALLY, these fields must also be an IFACE for the field type! -// -// // NOTE: The @Rmi field type will already have to be a registered type with kryo! -// // we can use this information on WHERE to scan for classes. -// val filesToScan = mutableSetOf() -// -// classesToRegister.forEach { registration -> -// val clazz = registration.clazz -// -// // can't do anything if codeSource is null! -// val codeSource = clazz.protectionDomain.codeSource ?: return@forEach -// // file:/Users/home/java/libs/xyz-123.jar -// // file:/projects/classes -// val jarOrClassPath = codeSource.location.toString() -// -// if (jarOrClassPath.endsWith(".jar")) { -// val fileName: String = URLDecoder.decode(jarOrClassPath.substring("file:".length), Charset.defaultCharset()) -// filesToScan.add(File(fileName).absoluteFile) -// } else { -// val classPath: String = URLDecoder.decode(jarOrClassPath.substring("file:".length), Charset.defaultCharset()) -// filesToScan.add(File(classPath).absoluteFile) -// } -// } -// -// val toTypedArray = filesToScan.toTypedArray() -// if (logger.isTraceEnabled) { -// toTypedArray.forEach { -// logger.trace { "Adding location to annotation scanner: $it"} -// } -// } -// -// -// -// // now scan these jars/directories -// val fieldsWithRmiAnnotation = AnnotationDetector.scanFiles(*toTypedArray) -// .forAnnotations(Rmi::class.java) -// .on(ElementType.FIELD) -// .collect { cursor -> Pair(cursor.type, cursor.field!!) } -// -// // have to make sure that the field type is specified as an interface (and not an implementation) -// fieldsWithRmiAnnotation.forEach { pair -> -// require(pair.second.type.isInterface) { "@Rmi annotated fields must be an interface!" } -// } -// -// if (fieldsWithRmiAnnotation.isNotEmpty()) { -// logger.info { "Verifying scanned classes containing @Rmi field annotations" } -// } -// -// // have to put this in a map, so we can quickly lookup + get the fields later on. -// // NOTE: a single class can have MULTIPLE fields with @Rmi annotations! -// val rmiAnnotationMap = IdentityMap, MutableList>() -// fieldsWithRmiAnnotation.forEach { -// var fields = rmiAnnotationMap[it.first] -// if (fields == null) { -// fields = mutableListOf() -// } -// -// fields.add(it.second) -// rmiAnnotationMap.put(it.first, fields) -// } -// -// // now make it an array for fast lookup for the [parent class] -> [annotated fields] -// rmiAnnotationMap.forEach { -// classesWithRmiFields.put(it.key, it.value.toTypedArray()) -// } -// -// // this will set up the class registration information -// initKryo() -// -// // now everything is REGISTERED, possibly with custom serializers, we have to go back and change them to use our RmiFieldSerializer -// fieldsWithRmiAnnotation.forEach FIELD_SCAN@{ pair -> -// // the parent class must be an IMPL. The reason is that THIS FIELD will be sent as a RMI object, and this can only -// // happen on objects that exist -// -// // NOTE: it IS necessary for the rmi-client to be aware of the @Rmi annotation (because it also has to have the correct serialization) -// -// // also, it is possible for the class that has the @Rmi field to be a NORMAL object (and not an RMI object) -// // this means we found the registration for the @Rmi field annotation -// -// val parentRmiRegistration = classesToRegister.firstOrNull { it is ClassRegistrationForRmi && it.implClass == pair.first} -// -// -// // if we have a parent-class registration, this means we are the rmi-server -// // -// // AND BECAUSE OF THIS -// // -// // we must also have the field type registered as RMI -// if (parentRmiRegistration != null) { -// // rmi-server -// -// // is the field type registered also? -// val fieldRmiRegistration = classesToRegister.firstOrNull { it.clazz == pair.second.type} -// require(fieldRmiRegistration is ClassRegistrationForRmi) { "${pair.second.type} is not registered for RMI! Unable to continue"} -// -// logger.trace { "Found @Rmi field annotation '${pair.second.type}' in class '${pair.first}'" } -// } else { -// // rmi-client -// -// // NOTE: rmi-server MUST have the field IMPL registered (ie: via RegisterRmi) -// // rmi-client will have the serialization updated from the rmi-server during connection handshake -// } -// } - } - - - /** - * Split array into chunks, max of 256 chunks. - * byte[0] = chunk ID - * byte[1] = total chunks (0-255) (where 0->1, 2->3, 127->127 because this is indexed by a byte) - */ - private fun divideArray(source: ByteArray, chunksize: Int): Array? { - val fragments = ceil(source.size / chunksize.toDouble()).toInt() - if (fragments > 127) { - // cannot allow more than 127 - return null - } - - // pre-allocate the memory - val splitArray = Array(fragments) { ByteArray(chunksize + 2) } - var start = 0 - for (i in splitArray.indices) { - var length = if (start + chunksize > source.size) { - source.size - start - } else { - chunksize - } - splitArray[i] = ByteArray(length + 2) - splitArray[i][0] = i.toByte() // index - splitArray[i][1] = fragments.toByte() // total number of fragments - System.arraycopy(source, start, splitArray[i], 2, length) - start += chunksize - } - return splitArray - } -} - -// fun initClassRegistration(channel: Channel, registration: Registration): Boolean { -// val details = serialization.getKryoRegistrationDetails() -// val length = details.size -// if (length > Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE) { -// // it is too large to send in a single packet -// -// // child arrays have index 0 also as their 'index' and 1 is the total number of fragments -// val fragments = divideArray(details, Serialization.CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE) -// if (fragments == null) { -// logger.error("Too many classes have been registered for Serialization. Please report this issue") -// return false -// } -// val allButLast = fragments.size - 1 -// for (i in 0 until allButLast) { -// val fragment = fragments[i] -// val fragmentedRegistration = Registration.hello(registration.oneTimePad, config.settingsStore.getPublicKey()) -// fragmentedRegistration.payload = fragment -// -// // tell the server we are fragmented -// fragmentedRegistration.upgradeType = UpgradeType.FRAGMENTED -// -// // tell the server we are upgraded (it will bounce back telling us to connect) -// fragmentedRegistration.upgraded = true -// channel.writeAndFlush(fragmentedRegistration) -// } -// -// // now tell the server we are done with the fragments -// val fragmentedRegistration = Registration.hello(registration.oneTimePad, config.settingsStore.getPublicKey()) -// fragmentedRegistration.payload = fragments[allButLast] -// -// // tell the server we are fragmented -// fragmentedRegistration.upgradeType = UpgradeType.FRAGMENTED -// -// // tell the server we are upgraded (it will bounce back telling us to connect) -// fragmentedRegistration.upgraded = true -// channel.writeAndFlush(fragmentedRegistration) -// } else { -// registration.payload = details -// -// // tell the server we are upgraded (it will bounce back telling us to connect) -// registration.upgraded = true -// channel.writeAndFlush(registration) -// } -// return true -// } - - diff --git a/not-fixed/MultipleThreadTest.java b/not-fixed/MultipleThreadTest.java deleted file mode 100755 index 98e73fc6..00000000 --- a/not-fixed/MultipleThreadTest.java +++ /dev/null @@ -1,242 +0,0 @@ -/* Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import static org.junit.Assert.assertEquals; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import dorkbox.network.connection.Connection; -import dorkbox.network.connection.Listener; -import dorkbox.network.connection.Listeners; -import dorkbox.network.serialization.Serialization; -import dorkbox.util.exceptions.SecurityException; - -public -class MultipleThreadTest extends BaseTest { - private final Object lock = new Object(); - private volatile boolean stillRunning = false; - - private final Object finalRunLock = new Object(); - private volatile boolean finalStillRunning = false; - - private final int messageCount = 150; - private final int threadCount = 15; - private final int clientCount = 13; - - private final List clients = new ArrayList(this.clientCount); - - int perClientReceiveTotal = (this.messageCount * this.threadCount); - int serverReceiveTotal = perClientReceiveTotal * this.clientCount; - - AtomicInteger sent = new AtomicInteger(0); - AtomicInteger totalClientReceived = new AtomicInteger(0); - AtomicInteger receivedServer = new AtomicInteger(1); - - ConcurrentHashMap sentStringsToClientDebug = new ConcurrentHashMap(); - - @Test - public - void multipleThreads() throws SecurityException, IOException { - // our clients should receive messageCount * threadCount * clientCount TOTAL messages - final int totalClientReceivedCountExpected = this.clientCount * this.messageCount * this.threadCount; - final int totalServerReceivedCountExpected = this.clientCount * this.messageCount; - - System.err.println("CLIENT RECEIVES: " + totalClientReceivedCountExpected); - System.err.println("SERVER RECEIVES: " + totalServerReceivedCountExpected); - - - Configuration configuration = new Configuration(); - configuration.tcpPort = tcpPort; - configuration.host = host; - configuration.serialization.register(String[].class); - configuration.serialization.register(DataClass.class); - - - final Server server = new Server(configuration); - server.disableRemoteKeyValidation(); - - addEndPoint(server); - server.bind(false); - - - final Listeners listeners = server.listeners(); - listeners.add(new Listener.OnConnected() { - - @Override - public - void connected(final Connection connection) { - System.err.println("Client connected to server."); - - // kickoff however many threads we need, and send data to the client. - for (int i = 1; i <= MultipleThreadTest.this.threadCount; i++) { - final int index = i; - new Thread() { - @Override - public - void run() { - for (int i = 1; i <= MultipleThreadTest.this.messageCount; i++) { - int incrementAndGet = MultipleThreadTest.this.sent.getAndIncrement(); - DataClass dataClass = new DataClass("Server -> client. Thread #" + index + " message# " + incrementAndGet, - incrementAndGet); - - //System.err.println(dataClass.data); - MultipleThreadTest.this.sentStringsToClientDebug.put(incrementAndGet, dataClass); - connection.send() - .TCP(dataClass) - .flush(); - } - - } - }.start(); - } - } - }); - - listeners.add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, DataClass object) { - int incrementAndGet = MultipleThreadTest.this.receivedServer.getAndIncrement(); - //System.err.println("server #" + incrementAndGet); - - - if (incrementAndGet % MultipleThreadTest.this.messageCount == 0) { - System.err.println("Server receive DONE for client " + incrementAndGet); - - stillRunning = false; - synchronized (MultipleThreadTest.this.lock) { - MultipleThreadTest.this.lock.notifyAll(); - } - } - - if (incrementAndGet == totalServerReceivedCountExpected) { - System.err.println("Server DONE: " + incrementAndGet); - - finalStillRunning = false; - synchronized (MultipleThreadTest.this.finalRunLock) { - MultipleThreadTest.this.finalRunLock.notifyAll(); - } - } - } - }); - - // ---- - finalStillRunning = true; - for (int i = 1; i <= this.clientCount; i++) { - final int index = i; - - Client client = new Client(configuration); - this.clients.add(client); - - addEndPoint(client); - client.listeners() - .add(new Listener.OnMessageReceived() { - final int clientIndex = index; - final AtomicInteger received = new AtomicInteger(1); - - @Override - public - void received(Connection connection, DataClass object) { - totalClientReceived.getAndIncrement(); - int clientLocalCounter = this.received.getAndIncrement(); - MultipleThreadTest.this.sentStringsToClientDebug.remove(object.index); - - //System.err.println(object.data); - // we finished!! - if (clientLocalCounter == perClientReceiveTotal) { - //System.err.println("Client #" + clientIndex + " received " + clientLocalCounter + " Sending back " + - // MultipleThreadTest.this.messageCount + " messages."); - - // now spam back messages! - for (int i = 0; i < MultipleThreadTest.this.messageCount; i++) { - connection.send() - .TCP(new DataClass("Client #" + clientIndex + " -> Server message " + i, index)); - } - } - } - }); - - - stillRunning = true; - - client.connect(5000); - - while (stillRunning) { - synchronized (this.lock) { - try { - this.lock.wait(5 * 1000); // 5 secs - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - } - - while (finalStillRunning) { - synchronized (this.finalRunLock) { - try { - this.finalRunLock.wait(5 * 1000); // 5 secs - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - // CLIENT will wait until it's done connecting, but SERVER is async. - // the ONLY way to safely work in the server is with LISTENERS. Everything else can FAIL, because of it's async nature. - - if (!this.sentStringsToClientDebug.isEmpty()) { - System.err.println("MISSED DATA: " + this.sentStringsToClientDebug.size()); - for (Map.Entry i : this.sentStringsToClientDebug.entrySet()) { - System.err.println(i.getKey() + " : " + i.getValue().data); - } - } - - stopEndPoints(); - assertEquals(totalClientReceivedCountExpected, totalClientReceived.get()); - - // offset by 1 since we start at 1 - assertEquals(totalServerReceivedCountExpected, receivedServer.get()-1); - } - - - public static - class DataClass { - public String data; - public Integer index; - - public - DataClass() { - } - - public - DataClass(String data, Integer index) { - this.data = data; - this.index = index; - } - } -} diff --git a/not-fixed/PingPongLocalTest.java b/not-fixed/PingPongLocalTest.java deleted file mode 100755 index 88f87325..00000000 --- a/not-fixed/PingPongLocalTest.java +++ /dev/null @@ -1,326 +0,0 @@ -/* Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import dorkbox.network.connection.Connection; -import dorkbox.network.connection.Listener; -import dorkbox.network.connection.Listeners; -import dorkbox.network.serialization.Serialization; -import dorkbox.util.exceptions.SecurityException; -import dorkbox.util.serialization.SerializationManager; - -public -class PingPongLocalTest extends BaseTest { - int tries = 10000; - private volatile String fail; - - @Test - public void pingPongLocal() throws SecurityException, IOException { - this.fail = "Data not received."; - - final Data dataLOCAL = new Data(); - populateData(dataLOCAL); - - Configuration configuration = Configuration.localOnly(); - register(configuration.serialization); - - - Server server = new Server(configuration); - addEndPoint(server); - server.bind(false); - final Listeners listeners = server.listeners(); - listeners.add(new Listener.OnError() { - @Override - public - void error(Connection connection, Throwable throwable) { - PingPongLocalTest.this.fail = "Error during processing. " + throwable; - } - }); - listeners.add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, Data data) { - connection.id(); - if (!data.equals(dataLOCAL)) { - PingPongLocalTest.this.fail = "data is not equal on server."; - throw new RuntimeException("Fail! " + PingPongLocalTest.this.fail); - } - connection.send() - .TCP(data); - } - }); - - // ---- - - Client client = new Client(configuration); - addEndPoint(client); - final Listeners listeners1 = client.listeners(); - listeners1.add(new Listener.OnConnected() { - @Override - public - void connected(Connection connection) { - PingPongLocalTest.this.fail = null; - connection.send() - .TCP(dataLOCAL); - // connection.sendUDP(dataUDP); // TCP and UDP are the same for a local channel. - } - }); - - listeners1.add(new Listener.OnError() { - @Override - public - void error(Connection connection, Throwable throwable) { - PingPongLocalTest.this.fail = "Error during processing. " + throwable; - System.err.println(PingPongLocalTest.this.fail); - } - }); - - listeners1.add(new Listener.OnMessageReceived() { - AtomicInteger check = new AtomicInteger(0); - - @Override - public - void received(Connection connection, Data data) { - if (!data.equals(dataLOCAL)) { - PingPongLocalTest.this.fail = "data is not equal on client."; - throw new RuntimeException("Fail! " + PingPongLocalTest.this.fail); - } - - if (this.check.getAndIncrement() <= PingPongLocalTest.this.tries) { - connection.send() - .TCP(data); - } - else { - System.err.println("Ran LOCAL " + PingPongLocalTest.this.tries + " times"); - stopEndPoints(); - } - } - }); - - client.connect(5000); - - waitForThreads(); - - if (this.fail != null) { - fail(this.fail); - } - } - - private void populateData(Data data) { - StringBuilder buffer = new StringBuilder(); - for (int i = 0; i < 3000; i++) { - buffer.append('a'); - } - data.string = buffer.toString(); - - data.strings = new String[] {"abcdefghijklmnopqrstuvwxyz0123456789","",null,"!@#$","�����"}; - data.ints = new int[] {-1234567,1234567,-1,0,1,Integer.MAX_VALUE,Integer.MIN_VALUE}; - data.shorts = new short[] {-12345,12345,-1,0,1,Short.MAX_VALUE,Short.MIN_VALUE}; - data.floats = new float[] {0,-0,1,-1,123456,-123456,0.1f,0.2f,-0.3f,(float) Math.PI,Float.MAX_VALUE, - Float.MIN_VALUE}; - - data.doubles = new double[] {0,-0,1,-1,123456,-123456,0.1d,0.2d,-0.3d,Math.PI,Double.MAX_VALUE,Double.MIN_VALUE}; - data.longs = new long[] {0,-0,1,-1,123456,-123456,99999999999l,-99999999999l,Long.MAX_VALUE,Long.MIN_VALUE}; - data.bytes = new byte[] {-123,123,-1,0,1,Byte.MAX_VALUE,Byte.MIN_VALUE}; - data.chars = new char[] {32345,12345,0,1,63,Character.MAX_VALUE,Character.MIN_VALUE}; - - data.booleans = new boolean[] {true,false}; - data.Ints = new Integer[] {-1234567,1234567,-1,0,1,Integer.MAX_VALUE,Integer.MIN_VALUE}; - data.Shorts = new Short[] {-12345,12345,-1,0,1,Short.MAX_VALUE,Short.MIN_VALUE}; - data.Floats = new Float[] {0f,-0f,1f,-1f,123456f,-123456f,0.1f,0.2f,-0.3f,(float) Math.PI,Float.MAX_VALUE, - Float.MIN_VALUE}; - data.Doubles = new Double[] {0d,-0d,1d,-1d,123456d,-123456d,0.1d,0.2d,-0.3d,Math.PI,Double.MAX_VALUE, - Double.MIN_VALUE}; - data.Longs = new Long[] {0l,-0l,1l,-1l,123456l,-123456l,99999999999l,-99999999999l,Long.MAX_VALUE, - Long.MIN_VALUE}; - data.Bytes = new Byte[] {-123,123,-1,0,1,Byte.MAX_VALUE,Byte.MIN_VALUE}; - data.Chars = new Character[] {32345,12345,0,1,63,Character.MAX_VALUE,Character.MIN_VALUE}; - data.Booleans = new Boolean[] {true,false}; - } - - private void register(SerializationManager manager) { - manager.register(int[].class); - manager.register(short[].class); - manager.register(float[].class); - manager.register(double[].class); - manager.register(long[].class); - manager.register(byte[].class); - manager.register(char[].class); - manager.register(boolean[].class); - manager.register(String[].class); - manager.register(Integer[].class); - manager.register(Short[].class); - manager.register(Float[].class); - manager.register(Double[].class); - manager.register(Long[].class); - manager.register(Byte[].class); - manager.register(Character[].class); - manager.register(Boolean[].class); - manager.register(Data.class); - } - - static public class Data { - public String string; - - public String[] strings; - - public int[] ints; - - public short[] shorts; - - public float[] floats; - - public double[] doubles; - - public long[] longs; - - public byte[] bytes; - - public char[] chars; - - public boolean[] booleans; - - public Integer[] Ints; - - public Short[] Shorts; - - public Float[] Floats; - - public Double[] Doubles; - - public Long[] Longs; - - public Byte[] Bytes; - - public Character[] Chars; - - public Boolean[] Booleans; - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(this.Booleans); - result = prime * result + Arrays.hashCode(this.Bytes); - result = prime * result + Arrays.hashCode(this.Chars); - result = prime * result + Arrays.hashCode(this.Doubles); - result = prime * result + Arrays.hashCode(this.Floats); - result = prime * result + Arrays.hashCode(this.Ints); - result = prime * result + Arrays.hashCode(this.Longs); - result = prime * result + Arrays.hashCode(this.Shorts); - result = prime * result + Arrays.hashCode(this.booleans); - result = prime * result + Arrays.hashCode(this.bytes); - result = prime * result + Arrays.hashCode(this.chars); - result = prime * result + Arrays.hashCode(this.doubles); - result = prime * result + Arrays.hashCode(this.floats); - result = prime * result + Arrays.hashCode(this.ints); - result = prime * result + Arrays.hashCode(this.longs); - result = prime * result + Arrays.hashCode(this.shorts); - result = prime * result + (this.string == null ? 0 : this.string.hashCode()); - result = prime * result + Arrays.hashCode(this.strings); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - Data other = (Data) obj; - if (!Arrays.equals(this.Booleans, other.Booleans)) { - return false; - } - if (!Arrays.equals(this.Bytes, other.Bytes)) { - return false; - } - if (!Arrays.equals(this.Chars, other.Chars)) { - return false; - } - if (!Arrays.equals(this.Doubles, other.Doubles)) { - return false; - } - if (!Arrays.equals(this.Floats, other.Floats)) { - return false; - } - if (!Arrays.equals(this.Ints, other.Ints)) { - return false; - } - if (!Arrays.equals(this.Longs, other.Longs)) { - return false; - } - if (!Arrays.equals(this.Shorts, other.Shorts)) { - return false; - } - if (!Arrays.equals(this.booleans, other.booleans)) { - return false; - } - if (!Arrays.equals(this.bytes, other.bytes)) { - return false; - } - if (!Arrays.equals(this.chars, other.chars)) { - return false; - } - if (!Arrays.equals(this.doubles, other.doubles)) { - return false; - } - if (!Arrays.equals(this.floats, other.floats)) { - return false; - } - if (!Arrays.equals(this.ints, other.ints)) { - return false; - } - if (!Arrays.equals(this.longs, other.longs)) { - return false; - } - if (!Arrays.equals(this.shorts, other.shorts)) { - return false; - } - if (this.string == null) { - if (other.string != null) { - return false; - } - } else if (!this.string.equals(other.string)) { - return false; - } - if (!Arrays.equals(this.strings, other.strings)) { - return false; - } - return true; - } - - @Override - public String toString() { - return "Data"; - } - } -} diff --git a/not-fixed/PooledSerialization.kt b/not-fixed/PooledSerialization.kt deleted file mode 100644 index ed381212..00000000 --- a/not-fixed/PooledSerialization.kt +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2014 dorkbox, llc - * - * 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 dorkbox.network.other - -import com.conversantmedia.util.concurrent.MultithreadConcurrentQueue -import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.Serializer -import com.esotericsoftware.minlog.Log -import dorkbox.network.serialization.ClassRegistration -import dorkbox.network.serialization.ClassRegistration0 -import dorkbox.network.serialization.ClassRegistration1 -import dorkbox.network.serialization.ClassRegistration2 -import dorkbox.network.serialization.ClassRegistration3 -import dorkbox.network.serialization.KryoExtra -import dorkbox.util.serialization.SerializationDefaults -import kotlinx.atomicfu.atomic - -class PooledSerialization { - companion object { - init { - Log.set(Log.LEVEL_ERROR) - } - } - - private var initialized = atomic(false) - private val classesToRegister = mutableListOf() - - private var kryoPoolSize = 16 - private val kryoInUse = atomic(0) - - @Volatile - private var kryoPool = MultithreadConcurrentQueue(kryoPoolSize) - - /** - * If you customize anything, you will want to register custom types before init() is called! - */ - fun init() { - // NOTE: there are problems if our serializer is THE SAME serializer used by the network stack! - // We are explicitly differet types to prevent that form happening - - initialized.value = true - } - - private fun initKryo(): KryoExtra { - val kryo = KryoExtra() - - SerializationDefaults.register(kryo) - - classesToRegister.forEach { registration -> - registration.register(kryo) - } - - return kryo - } - - - /** - * Registers the class using the lowest, next available integer ID and the [default serializer][Kryo.getDefaultSerializer]. - * If the class is already registered, the existing entry is updated with the new serializer. - * - * - * Registering a primitive also affects the corresponding primitive wrapper. - * - * Because the ID assigned is affected by the IDs registered before it, the order classes are registered is important when using this - * method. - * - * The order must be the same at deserialization as it was for serialization. - * - * This must happen before the creation of the client/server - */ - fun register(clazz: Class): PooledSerialization { - require(!initialized.value) { "Serialization 'register(class)' cannot happen after initialization!" } - - // The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather - // with object types... EVEN IF THERE IS A SERIALIZER - require(!clazz.isInterface) { "Cannot register '${clazz}' with specified ID for serialization. It must be an implementation." } - - classesToRegister.add(ClassRegistration3(clazz)) - return this - } - - /** - * Registers the class using the specified ID. If the ID is already in use by the same type, the old entry is overwritten. If the ID - * is already in use by a different type, an exception is thrown. - * - * - * Registering a primitive also affects the corresponding primitive wrapper. - * - * IDs must be the same at deserialization as they were for serialization. - * - * This must happen before the creation of the client/server - * - * @param id Must be >= 0. Smaller IDs are serialized more efficiently. IDs 0-8 are used by default for primitive types and String, but - * these IDs can be repurposed. - */ - fun register(clazz: Class, id: Int): PooledSerialization { - require(!initialized.value) { "Serialization 'register(Class, int)' cannot happen after initialization!" } - - // The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather - // with object types... EVEN IF THERE IS A SERIALIZER - require(!clazz.isInterface) { "Cannot register '${clazz}' with specified ID for serialization. It must be an implementation." } - - classesToRegister.add(ClassRegistration1(clazz, id)) - return this - } - - /** - * Registers the class using the lowest, next available integer ID and the specified serializer. If the class is already registered, - * the existing entry is updated with the new serializer. - * - * - * Registering a primitive also affects the corresponding primitive wrapper. - * - * - * Because the ID assigned is affected by the IDs registered before it, the order classes are registered is important when using this - * method. The order must be the same at deserialization as it was for serialization. - */ - @Synchronized - fun register(clazz: Class, serializer: Serializer): PooledSerialization { - require(!initialized.value) { "Serialization 'register(Class, Serializer)' cannot happen after initialization!" } - - // The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather - // with object types... EVEN IF THERE IS A SERIALIZER - require(!clazz.isInterface) { "Cannot register '${clazz.name}' with a serializer. It must be an implementation." } - - classesToRegister.add(ClassRegistration0(clazz, serializer)) - return this - } - - /** - * Registers the class using the specified ID and serializer. If the ID is already in use by the same type, the old entry is - * overwritten. If the ID is already in use by a different type, an exception is thrown. - * - * - * Registering a primitive also affects the corresponding primitive wrapper. - * - * - * IDs must be the same at deserialization as they were for serialization. - * - * @param id Must be >= 0. Smaller IDs are serialized more efficiently. IDs 0-8 are used by default for primitive types and String, but - * these IDs can be repurposed. - */ - @Synchronized - fun register(clazz: Class, serializer: Serializer, id: Int): PooledSerialization { - require(!initialized.value) { "Serialization 'register(Class, Serializer, int)' cannot happen after initialization!" } - - // The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather - // with object types... EVEN IF THERE IS A SERIALIZER - require(!clazz.isInterface) { "Cannot register '${clazz.name}'. It must be an implementation." } - - classesToRegister.add(ClassRegistration2(clazz, serializer, id)) - return this - } - - /** - * @return takes a kryo instance from the pool, or creates one if the pool was empty - */ - fun takeKryo(): KryoExtra { - kryoInUse.getAndIncrement() - - // ALWAYS get as many as needed. We recycle them (with an auto-growing pool) to prevent too many getting created - return kryoPool.poll() ?: initKryo() - } - - /** - * Returns a kryo instance to the pool for re-use later on - */ - fun returnKryo(kryo: KryoExtra) { - val kryoCount = kryoInUse.getAndDecrement() - if (kryoCount > kryoPoolSize) { - // this is CLEARLY a problem, as we have more kryos in use that our pool can support. - // This happens when we send messages REALLY fast. - // - // We fix this by increasing the size of the pool, so kryos aren't thrown away (and create a GC hit) - - synchronized(kryoInUse) { - // we have a double check here on purpose. only 1 will work - if (kryoCount > kryoPoolSize) { - val oldPool = kryoPool - val oldSize = kryoPoolSize - val newSize = kryoPoolSize * 2 - - kryoPoolSize = newSize - kryoPool = MultithreadConcurrentQueue(kryoPoolSize) - - - // take all of the old kryos and put them in the new one - val array = arrayOfNulls(oldSize) - val count = oldPool.remove(array) - - for (i in 0 until count) { - kryoPool.offer(array[i]) - } - } - } - } - - kryoPool.offer(kryo) - } -} diff --git a/not-fixed/ReconnectTest.java b/not-fixed/ReconnectTest.java deleted file mode 100755 index b8b90876..00000000 --- a/not-fixed/ReconnectTest.java +++ /dev/null @@ -1,296 +0,0 @@ -/* Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import static org.junit.Assert.assertEquals; - -import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import dorkbox.network.connection.Connection; -import dorkbox.network.connection.Listener; -import dorkbox.network.connection.Listeners; -import dorkbox.util.exceptions.SecurityException; - -// NOTE: UDP is unreliable, EVEN ON LOOPBACK! So this can fail with UDP. TCP will never fail. -public -class ReconnectTest extends BaseTest { - private final AtomicInteger receivedCount = new AtomicInteger(0); - - private static final Logger logger = LoggerFactory.getLogger(ReconnectTest.class.getSimpleName()); - - @Test - public - void socketReuseUDP() throws IOException, SecurityException { - socketReuse(false, true); - } - - @Test - public - void socketReuseTCP() throws IOException, SecurityException { - socketReuse(true, false); - } - - @Test - public - void socketReuseTCPUDP() throws IOException, SecurityException { - socketReuse(true, true); - } - - private - void socketReuse(final boolean useTCP, final boolean useUDP) throws SecurityException, IOException { - receivedCount.set(0); - - Configuration configuration = new Configuration(); - configuration.host = host; - - if (useTCP) { - configuration.tcpPort = tcpPort; - } - - if (useUDP) { - configuration.udpPort = udpPort; - } - - AtomicReference latch = new AtomicReference(); - - - Server server = new Server(configuration); - addEndPoint(server); - final Listeners listeners = server.listeners(); - listeners.add(new Listener.OnConnected() { - @Override - public - void connected(Connection connection) { - if (useTCP) { - connection.send() - .TCP("-- TCP from server"); - } - if (useUDP) { - connection.send() - .UDP("-- UDP from server"); - } - } - }); - listeners.add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, String object) { - int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet(); - logger.error("----- " + incrementAndGet + " : " + object); - - latch.get().countDown(); - } - }); - server.bind(false); - - - // ---- - - Client client = new Client(configuration); - addEndPoint(client); - final Listeners listeners1 = client.listeners(); - listeners1.add(new Listener.OnConnected() { - @Override - public - void connected(Connection connection) { - if (useTCP) { - connection.send() - .TCP("-- TCP from client"); - } - if (useUDP) { - connection.send() - .UDP("-- UDP from client"); - } - } - }); - listeners1.add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, String object) { - int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet(); - logger.error("----- " + incrementAndGet + " : " + object); - - latch.get().countDown(); - } - }); - - - - int latchCount = 2; - int count = 100; - int initialCount = 2; - if (useTCP && useUDP) { - initialCount += 2; - latchCount += 2; - } - - - try { - for (int i = 1; i < count + 1; i++) { - logger.error("....."); - latch.set(new CountDownLatch(latchCount)); - - try { - client.connect(5000); - } catch (IOException e) { - e.printStackTrace(); - } - - int retryCount = 20; - int lastRetryCount; - - int target = i * initialCount; - boolean failed = false; - - synchronized (receivedCount) { - while (this.receivedCount.get() != target) { - lastRetryCount = this.receivedCount.get(); - - try { - latch.get().await(1, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - // check to see if we changed at all... - if (lastRetryCount == this.receivedCount.get()) { - if (retryCount-- < 0) { - logger.error("Aborting unit test... wrong count!"); - if (useUDP) { - // If TCP and UDP both fill the pipe, THERE WILL BE FRAGMENTATION and dropped UDP packets! - // it results in severe UDP packet loss and contention. - // - // http://www.isoc.org/INET97/proceedings/F3/F3_1.HTM - // also, a google search on just "INET97/proceedings/F3/F3_1.HTM" turns up interesting problems. - // Usually it's with ISPs. - - logger.error("NOTE: UDP can fail, even on loopback! See: http://www.isoc.org/INET97/proceedings/F3/F3_1.HTM"); - } - failed = true; - break; - } - } else { - retryCount = 20; - } - } - } - - client.close(); - logger.error("....."); - - if (failed) { - break; - } - } - - int specified = count * initialCount; - int received = this.receivedCount.get(); - - if (specified != received) { - logger.error("NOTE: UDP can fail, even on loopback! See: http://www.isoc.org/INET97/proceedings/F3/F3_1.HTM"); - } - - assertEquals(specified, received); - } finally { - stopEndPoints(); - waitForThreads(10); - } - } - - @Test - public - void localReuse() throws SecurityException, IOException { - receivedCount.set(0); - - Server server = new Server(); - addEndPoint(server); - server.listeners() - .add(new Listener.OnConnected() { - @Override - public - void connected(Connection connection) { - connection.send() - .self("-- LOCAL from server"); - } - }); - server.listeners() - .add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, String object) { - int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet(); - System.out.println("----- " + incrementAndGet + " : " + object); - } - }); - - // ---- - - Client client = new Client(); - addEndPoint(client); - client.listeners() - .add(new Listener.OnConnected() { - @Override - public - void connected(Connection connection) { - connection.send() - .self("-- LOCAL from client"); - } - }); - - client.listeners() - .add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, String object) { - int incrementAndGet = ReconnectTest.this.receivedCount.incrementAndGet(); - System.out.println("----- " + incrementAndGet + " : " + object); - } - }); - - server.bind(false); - int count = 10; - for (int i = 1; i < count + 1; i++) { - client.connect(5000); - - int target = i * 2; - while (this.receivedCount.get() != target) { - System.out.println("----- Waiting..."); - try { - Thread.sleep(100); - } catch (InterruptedException ignored) { - } - } - - client.close(); - } - - assertEquals(count * 2, this.receivedCount.get()); - - stopEndPoints(); - waitForThreads(10); - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index cce9d6cd..44832c4c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2018 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,3 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +rootProject.name = "Network" diff --git a/src/dorkbox/network/Client.kt b/src/dorkbox/network/Client.kt index f7cde135..d690bdad 100644 --- a/src/dorkbox/network/Client.kt +++ b/src/dorkbox/network/Client.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2024 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,41 +15,35 @@ */ package dorkbox.network -import dorkbox.bytes.toHexString import dorkbox.dns.DnsClient +import dorkbox.hex.toHexString import dorkbox.netUtil.IP import dorkbox.netUtil.IPv4 import dorkbox.netUtil.IPv6 -import dorkbox.netUtil.Inet4 -import dorkbox.netUtil.Inet6 import dorkbox.netUtil.dnsUtils.ResolvedAddressTypes import dorkbox.network.aeron.AeronDriver -import dorkbox.network.aeron.mediaDriver.ClientIpcDriver -import dorkbox.network.aeron.mediaDriver.ClientUdpDriver -import dorkbox.network.aeron.mediaDriver.MediaDriverClient -import dorkbox.network.aeron.mediaDriver.MediaDriverConnectInfo +import dorkbox.network.aeron.EventActionOperator +import dorkbox.network.aeron.EventCloseOperator +import dorkbox.network.aeron.EventPoller import dorkbox.network.connection.Connection import dorkbox.network.connection.ConnectionParams import dorkbox.network.connection.EndPoint -import dorkbox.network.connection.ListenerManager +import dorkbox.network.connection.IpInfo.Companion.formatCommonAddress +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTraceInternal import dorkbox.network.connection.PublicKeyValidationState -import dorkbox.network.exceptions.ClientException -import dorkbox.network.exceptions.ClientRejectedException -import dorkbox.network.exceptions.ClientRetryException -import dorkbox.network.exceptions.ClientShutdownException -import dorkbox.network.exceptions.ClientTimedOutException +import dorkbox.network.connection.buffer.BufferManager +import dorkbox.network.exceptions.* +import dorkbox.network.handshake.ClientConnectionDriver import dorkbox.network.handshake.ClientHandshake +import dorkbox.network.handshake.ClientHandshakeDriver import dorkbox.network.ping.Ping -import dorkbox.network.ping.PingManager -import kotlinx.atomicfu.atomic -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import java.lang.Thread.sleep +import dorkbox.util.Sys +import org.slf4j.LoggerFactory import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress +import java.util.* import java.util.concurrent.* /** @@ -57,88 +51,44 @@ import java.util.concurrent.* * ASYNC. * * @param config these are the specific connection options - * @param connectionFunc allows for custom connection implementations defined as a unit function * @param loggerName allows for a custom logger name for this endpoint (for when there are multiple endpoints) */ @Suppress("unused") -open class Client( - config: ClientConfiguration = ClientConfiguration(), - connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION, - loggerName: String = Client::class.java.simpleName) - : EndPoint(config, connectionFunc, loggerName) { - - /** - * The client is both SYNC and ASYNC. It starts off SYNC (blocks thread until it's done), then once it's connected to the server, it's - * ASYNC. - * - * @param config these are the specific connection options - * @param loggerName allows for a custom logger name for this endpoint (for when there are multiple endpoints) - * @param connectionFunc allows for custom connection implementations defined as a unit function - */ - constructor(config: ClientConfiguration, - loggerName: String, - connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION) - : this(config, connectionFunc, loggerName) - - - /** - * The client is both SYNC and ASYNC. It starts off SYNC (blocks thread until it's done), then once it's connected to the server, it's - * ASYNC. - * - * @param config these are the specific connection options - * @param connectionFunc allows for custom connection implementations defined as a unit function - */ - constructor(config: ClientConfiguration, - connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION) - : this(config, connectionFunc, Client::class.java.simpleName) - - - /** - * The client is both SYNC and ASYNC. It starts off SYNC (blocks thread until it's done), then once it's connected to the server, it's - * ASYNC. - * - * @param config these are the specific connection options - * @param loggerName allows for a custom logger name for this endpoint (for when there are multiple endpoints) - */ - constructor(config: ClientConfiguration, - loggerName: String) - : this(config, - { - @Suppress("UNCHECKED_CAST") - Connection(it) as CONNECTION - }, - loggerName) - - - /** - * The client is both SYNC and ASYNC. It starts off SYNC (blocks thread until it's done), then once it's connected to the server, it's - * ASYNC. - * - * @param config these are the specific connection options - */ - constructor(config: ClientConfiguration) - : this(config, - { - @Suppress("UNCHECKED_CAST") - Connection(it) as CONNECTION - }, - Client::class.java.simpleName) - - +open class Client(config: ClientConfiguration = ClientConfiguration(), loggerName: String = Client::class.java.simpleName) + : EndPoint(config, loggerName) { companion object { /** * Gets the version number. */ - const val version = "5.32" + const val version = Configuration.version + + /** + * Ensures that the client (using the specified configuration) is NO LONGER running. + * + * NOTE: This method should only be used to check if a client is running for a DIFFERENT configuration than the currently running client + * + * By default, we will wait the [Configuration.connectionCloseTimeoutInSeconds] * 2 amount of time before returning. + * + * @return true if the media driver is STOPPED. + */ + fun ensureStopped(configuration: Configuration): Boolean { + val timeout = TimeUnit.SECONDS.toMillis(configuration.connectionCloseTimeoutInSeconds.toLong() * 2) + + val logger = LoggerFactory.getLogger(Client::class.java.simpleName) + return AeronDriver.ensureStopped(configuration.copy(), logger, timeout) + } /** * Checks to see if a client (using the specified configuration) is running. * - * This method should only be used to check if a client is running for a DIFFERENT configuration than the currently running client + * NOTE: This method should only be used to check if a client is running for a DIFFERENT configuration than the currently running client + * + * @return true if the media driver is active and running */ fun isRunning(configuration: Configuration): Boolean { - return AeronDriver(configuration).isRunning() + val logger = LoggerFactory.getLogger(Client::class.java.simpleName) + return AeronDriver.isRunning(configuration.copy(), logger) } init { @@ -148,43 +98,156 @@ open class Client( } /** - * The network or IPC address for the client to connect to. - * - * For a network address, it can be: - * - a network name ("localhost", "loopback", "lo", "bob.example.org") - * - an IP address ("127.0.0.1", "123.123.123.123", "::1") - * - * For the IPC (Inter-Process-Communication) address. it must be: - * - the IPC integer ID, "0x1337c0de", "0x12312312", etc. + * The network address of the remote machine that the client connected to. This will be null for IPC connections. + */ + @Volatile + var address: InetAddress? = IPv4.LOCALHOST + private set + + /** + * The network address of the remote machine that the client connected to, as a string. This will be "IPC" for IPC connections. + */ + @Volatile + var addressString: String = "UNKNOWN" + private set + + /** + * The network address of the remote machine that the client connected to, as a pretty string. This will be "IPC" for IPC connections. + */ + @Volatile + var addressPrettyString: String = "UNKNOWN" + private set + + /** + * The tag name assigned (by the configuration) to the client. The server will receive this tag during the handshake. The max length is + * 32 characters. */ @Volatile - var remoteAddress: InetAddress? = IPv4.LOCALHOST + var tag: String = "" + private set + + + /** + * The default connection reliability type (ie: can the lower-level network stack throw away data that has errors, for example real-time-voice) + */ + @Volatile + var reliable: Boolean = true private set /** - * the remote address, as a string. + * How long (in seconds) will connections wait to connect. 0 will wait indefinitely, */ @Volatile - var remoteAddressString: String = "UNKNOWN" + var connectionTimeoutSec: Int = 0 private set + /** + * - if the client is internally going to reconnect (because of a network error) + * - we have specified that we will run the disconnect logic + * - there is reconnect logic in the disconnect handler + * + * Then ultimately, we want to ignore the disconnect-handler reconnect (we do not want to have multiple reconnects happening concurrently) + */ + @Volatile + private var autoReconnect = false + + private val handshake = ClientHandshake(this, logger) + + @Volatile + internal var clientConnectionInProgress = CountDownLatch(0) + + @Volatile + private var slowDownForException = false @Volatile - private var isConnected = false + private var stopConnectOnShutdown = false + + /** + * Different connections (to the same client) can be "buffered", meaning that if they "go down" because of a network glitch -- the data + * being sent is not lost (it is buffered) and then re-sent once the new connection is established. References to the old connection + * will also redirect to the new connection. + */ + @Volatile + internal var bufferedManager: BufferManager? = null // is valid when there is a connection to the server, otherwise it is null + @Volatile private var connection0: CONNECTION? = null - - // This is set by the client so if there is a "connect()" call in the the disconnect callback, we can have proper - // lock-stop ordering for how disconnect and connect work with each-other - // GUARANTEE that the callbacks for 'onDisconnect' happens-before the 'onConnect'. - private val lockStepForConnect = atomic(null) + private val string0: String by lazy { + "EndPoint [Client: ${storage.publicKey.toHexString()}]" + } final override fun newException(message: String, cause: Throwable?): Throwable { - return ClientException(message, cause) + // +2 because we do not want to see the stack for the abstract `newException` + val clientException = ClientException(message, cause) + clientException.cleanStackTrace(2) + return clientException + } + + /** + * Will attempt to re-connect to the server, with the settings previously used when calling connect() + * + * @throws IllegalArgumentException if the remote address is invalid + * @throws ClientTimedOutException if the client is unable to connect in x amount of time + * @throws ClientRejectedException if the client connection is rejected + * @throws ClientShutdownException if the client connection is shutdown while trying to connect + * @throws ClientException if there are misc errors + */ + @Suppress("DuplicatedCode") + fun reconnect() { + if (autoReconnect) { + // we must check if we should permit a MANUAL reconnect, because the auto-reconnect MIGHT ALSO re-connect! + + // autoReconnect will be "reset" when the connection closes. If in a happy state, then a manual reconnect is permitted. + logger.info("Ignoring reconnect, auto-reconnect is in progress") + return + } + + + if (connectionTimeoutSec == 0) { + logger.info("Reconnecting...") + } else { + logger.info("Reconnecting... (timeout in $connectionTimeoutSec seconds)") + } + + if (!isShutdown()) { + // if we aren't closed already, close now. + close(false) + waitForClose() + } + + connect( + remoteAddress = address, + remoteAddressString = addressString, + remoteAddressPrettyString = addressPrettyString, + port1 = port1, + port2 = port2, + connectionTimeoutSec = connectionTimeoutSec, + reliable = reliable, + ) + } + + /** + * Will attempt to connect via IPC to the server, with a default 30 second connection timeout and will block until completed. + * + * ### For the IPC (Inter-Process-Communication) it must be: + * - `connectIpc()` + * + * @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely + * + * @throws ClientTimedOutException if the client is unable to connect in x amount of time + */ + fun connectIpc(connectionTimeoutSec: Int = 30) { + connect(remoteAddress = null, // required! + port1 = 0, + port2 = 0, + remoteAddressString = IPC_NAME, + remoteAddressPrettyString = IPC_NAME, + connectionTimeoutSec = connectionTimeoutSec) } + /** * Will attempt to connect to the server, with a default 30 second connection timeout and will block until completed. * @@ -194,17 +257,23 @@ open class Client( * - a network name ("localhost", "bob.example.org") * - an IP address ("127.0.0.1", "123.123.123.123", "::1") * - an InetAddress address + * - `connect(LOCALHOST)` + * - `connect("localhost")` + * - `connect("bob.example.org")` + * - `connect("127.0.0.1")` + * - `connect("::1")` * * ### For the IPC (Inter-Process-Communication) it must be: - * - `connect()` - * - `connect("")` - * - `connectIpc()` + * - `connectIPC()` * * ### Case does not matter, and "localhost" is the default. * * @param remoteAddress The network or if localhost, IPC address for the client to connect to + * @param port1 The network host port1 to connect to + * @param port2 The network host port2 to connect to. The server uses this to work around NAT firewalls. By default, this is port1+1, + * but can also be configured independently. This is required, and must be different from port1. * @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely - * @param reliable true if we want to create a reliable connection (for UDP connections, is message loss acceptable?). + * @param reliable true if we want to create a reliable connection, can the lower-level network stack throw away data that has errors, (IE: real-time-voice traffic) * * @throws IllegalArgumentException if the remote address is invalid * @throws ClientTimedOutException if the client is unable to connect in x amount of time @@ -212,44 +281,26 @@ open class Client( */ fun connect( remoteAddress: InetAddress, + port1: Int, + port2: Int = port1+1, connectionTimeoutSec: Int = 30, - reliable: Boolean = true) - { + reliable: Boolean = true) { + val remoteAddressString = when (remoteAddress) { is Inet4Address -> IPv4.toString(remoteAddress) is Inet6Address -> IPv6.toString(remoteAddress, true) - else -> throw IllegalArgumentException("Cannot connect to $remoteAddress It is an invalid address!") + else -> throw IllegalArgumentException("Cannot connect to $remoteAddress It is an invalid address type!") } - - // Default IPC ports are flipped because they are in the perspective of the SERVER connect(remoteAddress = remoteAddress, remoteAddressString = remoteAddressString, + remoteAddressPrettyString = remoteAddressString, + port1 = port1, + port2 = port2, connectionTimeoutSec = connectionTimeoutSec, reliable = reliable) } - /** - * Will attempt to connect to the server via IPC, with a default 30 second connection timeout and will block until completed. - * - * @param ipcId The IPC publication address for the client to connect to - * @param ipcSubscriptionId The IPC subscription address for the client to connect to - * @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely. - * - * @throws IllegalArgumentException if the remote address is invalid - * @throws ClientTimedOutException if the client is unable to connect in x amount of time - * @throws ClientRejectedException if the client connection is rejected - */ - @Suppress("DuplicatedCode") - fun connectIpc( - ipcId: Int = AeronDriver.IPC_HANDSHAKE_STREAM_ID_SUB, - connectionTimeoutSec: Int = 30) - { - connect(remoteAddress = null, // required! - remoteAddressString = "IPC", - ipcId = ipcId, - connectionTimeoutSec = connectionTimeoutSec) - } /** * Will attempt to connect to the server, with a default 30 second connection timeout and will block until completed. @@ -260,122 +311,82 @@ open class Client( * - a network name ("localhost", "bob.example.org") * - an IP address ("127.0.0.1", "123.123.123.123", "::1") * - an InetAddress address - * - if no address is specified, and IPC is disabled in the config, then localhost will be selected + * - `connect(LOCALHOST)` + * - `connect("localhost")` + * - `connect("bob.example.org")` + * - `connect("127.0.0.1")` + * - `connect("::1")` * * ### For the IPC (Inter-Process-Communication) it must be: - * - `connect()` (only if ipc is enabled in the configuration) - * - `connect("")` (only if ipc is enabled in the configuration) - * - `connectIpc()` + * - `connectIPC()` * * ### Case does not matter, and "localhost" is the default. * - * @param remoteAddress The network host or ip address + * @param remoteAddress The network host name or ip address + * @param port1 The network host port1 to connect to + * @param port2 The network host port2 to connect to. The server uses this to work around NAT firewalls. By default, this is port1+1, + * but can also be configured independently. This is required, and must be different from port1. + * @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely * @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely - * @param reliable true if we want to create a reliable connection (for UDP connections, is message loss acceptable?). + * @param reliable true if we want to create a reliable connection, can the lower-level network stack throw away data that has errors, (IE: real-time-voice traffic) * * @throws IllegalArgumentException if the remote address is invalid * @throws ClientTimedOutException if the client is unable to connect in x amount of time * @throws ClientRejectedException if the client connection is rejected */ fun connect( - remoteAddress: String = "", + remoteAddress: String, + port1: Int, + port2: Int = port1+1, connectionTimeoutSec: Int = 30, - reliable: Boolean = true) - { + reliable: Boolean = true) { + fun connect(dnsResolveType: ResolvedAddressTypes) { + val ipv4Requested = dnsResolveType == ResolvedAddressTypes.IPV4_ONLY || dnsResolveType == ResolvedAddressTypes.IPV4_PREFERRED - when { - // this is default IPC settings - remoteAddress.isEmpty() && config.enableIpc -> { - connectIpc(connectionTimeoutSec = connectionTimeoutSec) - } - - config.enableIPv4 || IPv4.isPreferred -> { - // we have to check first if it's a valid IPv4 address. If not, maybe it's a DNS lookup - val inet4Address = if (IPv4.isValid(remoteAddress)) { - Inet4.toAddress(remoteAddress) - } else { + val inetAddress = formatCommonAddress(remoteAddress, ipv4Requested) { + // we already checked first if it's a valid IP address. This is called if it's not, since it might be a DNS lookup val client = DnsClient() - client.resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + client.resolvedAddressTypes(dnsResolveType) val records = client.resolve(remoteAddress) client.stop() records?.get(0) - } + } ?: throw IllegalArgumentException("The remote address '$remoteAddress' cannot be found.") - if (inet4Address == null) { - throw IllegalArgumentException("The remote address '$remoteAddress' cannot be found.") + val remoteAddressAsIp = IP.toString(inetAddress) + val formattedString = if (remoteAddress == remoteAddressAsIp) { + remoteAddress + } else { + "$remoteAddress ($remoteAddressAsIp)" } - connect(remoteAddress = inet4Address, - remoteAddressString = remoteAddress, + connect(remoteAddress = inetAddress, + // we check again, because the inetAddress that comes back from DNS, might not be what we expect + remoteAddressString = remoteAddressAsIp, + remoteAddressPrettyString = formattedString, + port1 = port1, + port2 = port2, connectionTimeoutSec = connectionTimeoutSec, reliable = reliable) } - config.enableIPv6 || IPv6.isPreferred -> { - // we have to check first if it's a valid IPv6 address. If not, maybe it's a DNS lookup - var inet6Address = if (IPv6.isValid(remoteAddress)) { - Inet6.toAddress(remoteAddress) - } else { - val client = DnsClient() - client.resolvedAddressTypes(ResolvedAddressTypes.IPV6_ONLY) - val records = client.resolve(remoteAddress) - client.stop() - records?.get(0) - } - - if (inet6Address == null) { - throw IllegalArgumentException("The remote address '$remoteAddress' cannot be found.") - } - - when (inet6Address) { - IPv4.LOCALHOST -> { - connect(remoteAddress = IPv6.LOCALHOST, - remoteAddressString = IPv6.toString(IPv6.LOCALHOST), - connectionTimeoutSec = connectionTimeoutSec, - reliable = reliable) - - } - is Inet4Address -> { - // we can map the IPv4 address to an IPv6 address. - val address = IPv6.toAddress(IPv4.toString(inet6Address), true)!! - connect(remoteAddress = address, - remoteAddressString = IPv6.toString(address), - connectionTimeoutSec = connectionTimeoutSec, - reliable = reliable) - } - else -> { - connect(remoteAddress = inet6Address, - remoteAddressString = remoteAddress, - connectionTimeoutSec = connectionTimeoutSec, - reliable = reliable) - } - } - } - - // if there is no preference, then try to connect via IPv4 - else -> { - // we have to check first if it's a valid IPv4 address. If not, maybe it's a DNS lookup - val inetAddress = if (IP.isValid(remoteAddress)) { - IP.toAddress(remoteAddress) - } else { - val client = DnsClient() - client.resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) - val records = client.resolve(remoteAddress) - client.stop() - records?.get(0) - } - - if (inetAddress == null) { - throw IllegalArgumentException("The remote address '$remoteAddress' cannot be found.") + when { + // this is default IPC settings + remoteAddress.isEmpty() && config.enableIpc -> { + connect(remoteAddress = null, // required! + port1 = 0, + port2 = 0, + remoteAddressString = IPC_NAME, + remoteAddressPrettyString = IPC_NAME, + connectionTimeoutSec = connectionTimeoutSec) } - connect(remoteAddress = inetAddress, - remoteAddressString = remoteAddress, - connectionTimeoutSec = connectionTimeoutSec, - reliable = reliable - ) + // IPv6 takes precedence ONLY if it's enabled manually + config.enableIPv6 -> { connect(ResolvedAddressTypes.IPV6_ONLY) } + config.enableIPv4 -> { connect(ResolvedAddressTypes.IPV4_ONLY) } + IPv4.isPreferred -> { connect(ResolvedAddressTypes.IPV4_PREFERRED) } + IPv6.isPreferred -> { connect(ResolvedAddressTypes.IPV6_PREFERRED) } + else -> { connect(ResolvedAddressTypes.IPV4_PREFERRED) } } - } } /** @@ -388,19 +399,24 @@ open class Client( * - a network name ("localhost", "bob.example.org") * - an IP address ("127.0.0.1", "123.123.123.123", "::1") * - an InetAddress address + * - `connect()` (same as localhost, but only if ipc is disabled in the configuration) + * - `connect("localhost")` + * - `connect("bob.example.org")` + * - `connect("127.0.0.1")` + * - `connect("::1")` * * ### For the IPC (Inter-Process-Communication) it must be: - * - `connect()` - * - `connect("")` - * - `connectIpc()` + * - `connect()` (only if ipc is enabled in the configuration) * * ### Case does not matter, and "localhost" is the default. * - * @param remoteAddress The network or if localhost, IPC address for the client to connect to - * @param ipcPublicationId The IPC publication address for the client to connect to - * @param ipcSubscriptionId The IPC subscription address for the client to connect to + * @param remoteAddress The network or if localhost for the client to connect to + * @param port1 The network host port1 to connect to + * @param port2 The network host port2 to connect to. The server uses this to work around NAT firewalls. By default, this is port1+1, + * but can also be configured independently. This is required, and must be different from port1. + * @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely * @param connectionTimeoutSec wait for x seconds. 0 will wait indefinitely. - * @param reliable true if we want to create a reliable connection (for UDP connections, is message loss acceptable?). + * @param reliable true if we want to create a reliable connection, can the lower-level network stack throw away data that has errors, (IE: real-time-voice traffic) * * @throws IllegalArgumentException if the remote address is invalid * @throws ClientTimedOutException if the client is unable to connect in x amount of time @@ -410,230 +426,343 @@ open class Client( */ @Suppress("DuplicatedCode") private fun connect( - remoteAddress: InetAddress? = null, + remoteAddress: InetAddress?, remoteAddressString: String, - // Default IPC ports are flipped because they are in the perspective of the SERVER - ipcId: Int = AeronDriver.IPC_HANDSHAKE_STREAM_ID_SUB, - connectionTimeoutSec: Int = 30, + remoteAddressPrettyString: String, + port1: Int, + port2: Int, + connectionTimeoutSec: Int, reliable: Boolean = true) - { - // NOTE: it is critical to remember that Aeron DOES NOT like running from coroutines! - config as ClientConfiguration - + { + // NOTE: it is critical to remember that Aeron DOES NOT like running from coroutines! + if ((config.enableIPv4 || config.enableIPv6) && remoteAddress != null) { + require(port1 != port2) { "port1 cannot be the same as port2" } + require(port1 > 0) { "port1 must be > 0" } + require(port2 > 0) { "port2 must be > 0" } + require(port1 < 65535) { "port1 must be < 65535" } + require(port2 < 65535) { "port2 must be < 65535" } + } require(connectionTimeoutSec >= 0) { "connectionTimeoutSec '$connectionTimeoutSec' is invalid. It must be >=0" } - if (isConnected) { - logger.error { "Unable to connect when already connected!" } + // only try to connect via IPv4 if we have a network interface that supports it! + if (remoteAddress is Inet4Address && !IPv4.isAvailable) { + require(false) { "Unable to connect to the IPv4 address $remoteAddressPrettyString, there are no IPv4 interfaces available!" } + } + + // only try to connect via IPv6 if we have a network interface that supports it! + if (remoteAddress is Inet6Address && !IPv6.isAvailable) { + require(false) { "Unable to connect to the IPv6 address $remoteAddressPrettyString, there are no IPv6 interfaces available!" } + } + + if (remoteAddress != null && remoteAddress.isAnyLocalAddress) { + require(false) { "Cannot connect to $remoteAddressPrettyString It is an invalid address!" } + } + + + // on the client, we must GUARANTEE that the disconnect/close completes before NEW connect begins. + // we will know this if we are running inside an INTERNAL dispatch that is NOT the connect dispatcher! + if (eventDispatch.isDispatch() && !eventDispatch.CONNECT.isDispatch()) { + // only re-dispatch if we are on the event dispatch AND it's not the CONNECT one + // if we are on an "outside" thread, then we don't care. + + // we have to guarantee that CLOSE finishes running first! If we are already on close, then this will just run after close is finished. + eventDispatch.CLOSE.launch { + eventDispatch.CONNECT.launch { + connect( + remoteAddress = remoteAddress, + remoteAddressString = remoteAddressString, + remoteAddressPrettyString = remoteAddressPrettyString, + port1 = port1, + port2 = port2, + connectionTimeoutSec = connectionTimeoutSec, + reliable = reliable) + } + } + + return + } + + + // the lifecycle of a client is the ENDPOINT (measured via the network event poller) and CONNECTION (measure from connection closed) + // if we are reconnecting, then we do not want to wait for the ENDPOINT to close first! + if (!waitForEndpointShutdown()) { + if (endpointIsRunning.value) { + listenerManager.notifyError(ServerException("Unable to start, the client is already running!")) + } else { + listenerManager.notifyError(ClientException("Unable to connect the client!")) + } return } + config as ClientConfiguration + + connection0 = null - // localhost/loopback IP might not always be 127.0.0.1 or ::1 - this.remoteAddress = remoteAddress - this.remoteAddressString = remoteAddressString // we are done with initial configuration, now initialize aeron and the general state of this endpoint + // this also makes sure that the dispatchers are still active. + // Calling `client.close()` will shutdown the dispatchers (and a new client instance must be created) try { + stopConnectOnShutdown = false startDriver() + initializeState() } catch (e: Exception) { - logger.error(e) { "Unable to start the network driver" } + listenerManager.notifyError(ClientException("Unable to start the client!", e)) + resetOnError() return } - // only try to connect via IPv4 if we have a network interface that supports it! - if (remoteAddress is Inet4Address && !IPv4.isAvailable) { - require(false) { "Unable to connect to the IPv4 address $remoteAddressString, there are no IPv4 interfaces available!" } - } + // localhost/loopback IP might not always be 127.0.0.1 or ::1 + // will be null if it's IPC + this.address = remoteAddress - // only try to connect via IPv6 if we have a network interface that supports it! - if (remoteAddress is Inet6Address && !IPv6.isAvailable) { - require(false) { "Unable to connect to the IPv6 address $remoteAddressString, there are no IPv6 interfaces available!" } - } + // will be exactly 'IPC' if it's IPC + // if it's an IP address, it will be the IP address + // if it's a DNS name, the name will be resolved, and it will be DNS (IP) + this.addressString = remoteAddressString + this.addressPrettyString = remoteAddressString + + this.port1 = port1 + this.port2 = port2 + + // DOUBLE CHECK! + require(config.tag.length <= 32) { "Client tag name length must be <= 32" } + this.tag = config.tag + + this.reliable = reliable + this.connectionTimeoutSec = connectionTimeoutSec + + val isSelfMachine = remoteAddress?.isLoopbackAddress == true || remoteAddress == lanAddress - if (remoteAddress != null && remoteAddress.isAnyLocalAddress) { - require(false) { "Cannot connect to $remoteAddressString It is an invalid address!" } - } // IPC can be enabled TWO ways! // - config.enableIpc // - NULL remoteAddress // It is entirely possible that the server does not have IPC enabled! val autoChangeToIpc = - (config.enableIpc && (remoteAddress == null || remoteAddress.isLoopbackAddress)) || (!config.enableIpc && remoteAddress == null) + (config.enableIpc && (remoteAddress == null || isSelfMachine)) || (!config.enableIpc && remoteAddress == null) + + // how long does the initial handshake take to connect + val aeronTimeoutNs = aeronDriver.publicationConnectionTimeoutNs() + aeronDriver.lingerNs() + var handshakeTimeoutNs = TimeUnit.SECONDS.toNanos(config.connectionCloseTimeoutInSeconds.toLong()) + if (handshakeTimeoutNs < aeronTimeoutNs) { + handshakeTimeoutNs += aeronTimeoutNs + logger.warn("Handshake timeout is less than aeron timeout, increasing timeout to ${Sys.getTimePrettyFull(handshakeTimeoutNs)} to prevent aeron timeout issues") + } + + // how long before we COMPLETELY give up retrying. A '0' means try forever. + var connectionTimoutInNs = TimeUnit.SECONDS.toNanos(connectionTimeoutSec.toLong()) + + if (connectionTimoutInNs in 1.. IPC" } - } + // ALSO, we want to make sure we DO NOT approach the linger timeout! + aeronDriver.delayLingerTimeout(2) + } - // MAYBE the server doesn't have IPC enabled? If no, we need to connect via network instead - val ipcConnection = ClientIpcDriver( - streamId = ipcId, - sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID, - localSessionId = localSessionId, - ) - - type = "${ipcConnection.type} '$remoteAddressString:$ipcId'" - - // throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports - try { - ipcConnection.build(aeronDriver, logger) - ipcConnection - } catch (e: Exception) { - if (remoteAddress == null) { - // if we specified that we MUST use IPC, then we have to throw the exception, because there is no IPC - val clientException = ClientException("Unable to connect via IPC to server. No address specified so fallback is unavailable", e) - ListenerManager.cleanStackTraceInternal(clientException) - throw clientException - } - logger.info { "IPC for loopback enabled, but unable to connect. Retrying with address $remoteAddressString" } - // try a UDP connection instead - val udpConnection = ClientUdpDriver( - address = remoteAddress, - addressString = remoteAddressString, - port = config.port, - streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID, - sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID, - localSessionId = localSessionId, - connectionTimeoutSec = connectionTimeoutSec, - isReliable = reliable - ) - - type = "${udpConnection.type} '$remoteAddressString:${config.port}'" + // the handshake connection is closed when the handshake has an error, or it is finished + var handshakeConnection: ClientHandshakeDriver? - // throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports - udpConnection.build(aeronDriver, logger) - udpConnection - } - } else { - val udpConnection = ClientUdpDriver( - address = remoteAddress!!, - addressString = remoteAddressString, - port = config.port, - streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID, - sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID, - localSessionId = localSessionId, - connectionTimeoutSec = handshakeTimeout, - isReliable = reliable - ) - - type = "${udpConnection.type} '$remoteAddressString:${config.port}'" - - // throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports - udpConnection.build(aeronDriver, logger) - udpConnection + try { + // always start the aeron driver inside the restart loop. + // If we've already started the driver (on the first "start"), then this does nothing (slowDownForException also make this not "double check") + if (slowDownForException) { + startDriver() } - logger.info { handshakeConnection.info } - + // throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports + handshakeConnection = ClientHandshakeDriver.build( + endpoint = this, + aeronDriver = aeronDriver, + autoChangeToIpc = autoChangeToIpc, + remoteAddress = remoteAddress, + remoteAddressString = remoteAddressString, + remotePort1 = port1, + clientListenPort = config.port, + remotePort2 = port2, + handshakeTimeoutNs = handshakeTimeoutNs, + connectionTimoutInNs = connectionTimoutInNs, + reliable = reliable, + tagName = tag, + logger = logger + ) - connect0(handshake, handshakeConnection, handshakeTimeout) - success = true + val pubSub = handshakeConnection.pubSub + val logInfo = pubSub.getLogInfo(logger.isDebugEnabled) + if (logger.isDebugEnabled) { + logger.debug("Creating new handshake to $logInfo") + } else { + logger.info("Creating new handshake to $logInfo") + } - // finished with the handshake, so always close the connection publication - // The subscription is RE-USED - handshakeConnection.publication.close() + connect0(handshake, handshakeConnection, handshakeTimeoutNs) + success = true + slowDownForException = false + clientConnectionInProgress.countDown() // once we're done with the connection process, stop trying break } catch (e: ClientRetryException) { - handshake.reset() - - // maybe the aeron driver isn't running? (or isn't running correctly?) + clientConnectionInProgress.countDown() aeronDriver.closeIfSingle() // if we are the ONLY instance using the media driver, restart it - aeronDriver.start() - // short delay, since it failed we want to limit the retry rate to something slower than "as fast as the CPU can do it" - // we also want to go at SLIGHTLY slower that the aeron driver timeout frequency, this way - if there are connection or handshake issues, the server has the chance to expire the connections. - // If we go TOO FAST, then the server will EVENTUALLY have aeron errors (since it can't keep up per client). We literally - // want to have 1 in-flight handshake, per connection attempt, during the aeron connection timeout + if (stopConnectOnShutdown) { + break + } + + val inSeconds = TimeUnit.NANOSECONDS.toSeconds(handshakeTimeoutNs) + val message = if (isIPC) { + "Unable to connect to IPC in $inSeconds seconds, retrying..." + } else { + "Unable to connect to $remoteAddressPrettyString ($port1|$port2) in $inSeconds seconds, retrying..." + } - // ALSO, we want to make sure we DO NOT approach the linger timeout! - sleep(aeronDriver.driverTimeout().coerceAtLeast(TimeUnit.NANOSECONDS.toSeconds(aeronDriver.getLingerNs()))) if (logger.isTraceEnabled) { - logger.trace(e) { "Unable to connect to $type, retrying..." } + logger.trace(message, e) } else { - logger.info { "Unable to connect to $type, retrying..." } + logger.info(message) + } + + slowDownForException = true + } catch (e: ClientRejectedException) { + clientConnectionInProgress.countDown() + aeronDriver.closeIfSingle() // if we are the ONLY instance using the media driver, stop it + + if (stopConnectOnShutdown) { + break } + slowDownForException = true + + if (e.cause is ServerException) { + resetOnError() + val cause = e.cause!! + val wrapped = ClientException(cause.message!!) + listenerManager.notifyError(wrapped) + throw wrapped + } else { + resetOnError() + listenerManager.notifyError(e) + throw e + } } catch (e: Exception) { - logger.error(e) { "[${handshake.connectKey}] : Un-recoverable error during handshake with $type. Aborting." } - handshake.reset() + clientConnectionInProgress.countDown() + aeronDriver.closeIfSingle() // if we are the ONLY instance using the media driver, restart it + + if (stopConnectOnShutdown) { + break + } + + val type = if (isIPC) { + "IPC" + } else { + "$remoteAddressPrettyString:$port1:$port2" + } + - listenerManager.notifyError(e) + listenerManager.notifyError(ClientException("[${handshake.connectKey}] : Un-recoverable error during handshake with $type. Aborting.", e)) + resetOnError() throw e } } if (!success) { - if (System.nanoTime() - startTime < timoutInNanos) { + endpointIsRunning.lazySet(false) + + if (stopConnectOnShutdown) { + val exception = ClientException("Client closed during connection attempt to '$remoteAddressString'. Aborting connection attempts.").cleanStackTrace(3) + listenerManager.notifyError(exception) + // if we are waiting for this connection to connect (on a different thread, for example), make sure to release it. + closeLatch.countDown() + throw exception + } + + if (System.nanoTime() - startTime < connectionTimoutInNs) { + val type = if (isIPC) { + "IPC" + } else { + "$remoteAddressPrettyString:$port1:$port2" + } + // we timed out. Throw the appropriate exception - val exception = ClientTimedOutException("Unable to connect to the server at $type in $connectionTimeoutSec seconds") - logger.error(exception) { "Aborting connection attempt to server." } + val exception = ClientTimedOutException("Unable to connect to the server at $type in $connectionTimeoutSec seconds, aborting connection attempt to server.") listenerManager.notifyError(exception) throw exception } // If we did not connect - throw an error. When `client.connect()` is called, either it connects or throws an error - val exception = ClientRejectedException("The server did not respond or permit the connection attempt") - ListenerManager.cleanStackTrace(exception) + val exception = ClientRejectedException("The server did not respond or permit the connection attempt within $connectionTimeoutSec seconds, aborting connection retry attempt to server.") + exception.cleanStackTrace() - logger.error(exception) { "Aborting connection retry attempt to server." } listenerManager.notifyError(exception) throw exception } } + + // the handshake process might have to restart this connection process. - private fun connect0(handshake: ClientHandshake, handshakeConnection: MediaDriverClient, connectionTimeoutSec: Int) { + private fun connect0(handshake: ClientHandshake, handshakeConnection: ClientHandshakeDriver, handshakeTimeoutNs: Long) { // this will block until the connection timeout, and throw an exception if we were unable to connect with the server - val isUsingIPC = handshakeConnection is ClientIpcDriver // throws(ConnectTimedOutException::class, ClientRejectedException::class, ClientException::class) - val connectionInfo = handshake.hello(handshakeConnection, connectionTimeoutSec) + val connectionInfo = handshake.hello( + tagName = tag, + endPoint = this, + handshakeConnection = handshakeConnection, + handshakeTimeoutNs = handshakeTimeoutNs + ) + + bufferedManager = BufferManager(config, listenerManager, aeronDriver, connectionInfo.sessionTimeout) + // VALIDATE:: check to see if the remote connection's public key has changed! - val validateRemoteAddress = if (isUsingIPC) { + val validateRemoteAddress = if (handshakeConnection.pubSub.isIpc) { PublicKeyValidationState.VALID } else { - crypto.validateRemoteAddress(remoteAddress!!, remoteAddressString, connectionInfo.publicKey) + crypto.validateRemoteAddress(address!!, addressString, connectionInfo.publicKey) } if (validateRemoteAddress == PublicKeyValidationState.INVALID) { - handshakeConnection.subscription.close() - handshakeConnection.publication.close() - + handshakeConnection.close(this) - val exception = ClientRejectedException("Connection to $remoteAddressString not allowed! Public key mismatch.") - logger.error(exception) { "Validation error" } + val exception = ClientRejectedException("Connection to [$addressString] not allowed! Public key mismatch.") + listenerManager.notifyError(exception) throw exception } @@ -643,206 +772,244 @@ open class Client( // is rogue, we do not want to carelessly provide info. - // we are now connected, so we can connect to the NEW client-specific ports - val clientConnection = if (isUsingIPC) { - // Create a subscription at the given address and port, using the given stream ID. - val driver = ClientIpcDriver(sessionId = connectionInfo.sessionId, - streamId = connectionInfo.port, - localSessionId = 1 // doesn't matter - ) - - driver.build(aeronDriver, logger) - logger.info { "Creating new IPC connection to $driver" } - driver.subscription.close() - - MediaDriverConnectInfo( - publication = driver.publication, - subscription = handshakeConnection.subscription, - subscriptionPort = driver.localSessionId, - publicationPort = driver.streamId, - streamId = 0, // this is because with IPC, we have stream sub/pub (which are replaced as port sub/pub) - sessionId = driver.remoteSessionId, - isReliable = driver.isReliable, - remoteAddress = null, - remoteAddressString = "ipc" - ) - } - else { - val driver = ClientUdpDriver( - address = (handshakeConnection as ClientUdpDriver).address, - addressString = handshakeConnection.addressString, - port = connectionInfo.port, // this is the port that we connect to - streamId = connectionInfo.streamId, - sessionId = connectionInfo.sessionId, - localSessionId = 1, // doesn't matter here - connectionTimeoutSec = connectionTimeoutSec, - isReliable = handshakeConnection.isReliable) - - // we have to construct how the connection will communicate! - // we don't care about the subscription, only the publication - driver.build(aeronDriver, logger) - logger.info { "Creating new connection to $driver" } - - driver.subscription.close() - - MediaDriverConnectInfo( - subscription = handshakeConnection.subscription, - publication = driver.publication, - subscriptionPort = 0, - publicationPort = driver.port, - streamId = driver.streamId, - sessionId = driver.remoteSessionId, - isReliable = driver.isReliable, - remoteAddress = driver.address, - remoteAddressString = IP.toString(driver.address) - ) - } - /////////////// //// RMI /////////////// - // we set up our kryo information once we connect to a server (using the server's kryo registration details) - if (!serialization.finishInit(type, connectionInfo.kryoRegistrationDetails)) { - handshakeConnection.subscription.close() - handshakeConnection.publication.close() + try { + // only have ot do one + serialization.finishClientConnect(connectionInfo.kryoRegistrationDetails) + } catch (e: Exception) { + handshakeConnection.close(this) // because we are getting the class registration details from the SERVER, this should never be the case. // It is still and edge case where the reconstruction of the registration details fails (maybe because of custom serializers) - val exception = if (isUsingIPC) { - ClientRejectedException("[${handshake.connectKey}] Connection to IPC has incorrect class registration details!!") + val exception = if (handshakeConnection.pubSub.isIpc) { + ClientRejectedException("[${handshake.connectKey}] Connection to IPC has incorrect class registration details!!", e) } else { - ClientRejectedException("[${handshake.connectKey}] Connection to $remoteAddressString has incorrect class registration details!!") - } - ListenerManager.cleanStackTraceInternal(exception) - throw exception - } - - val sessionId = clientConnection.sessionId - val streamId = clientConnection.streamId - val aeronLogInfo = "$sessionId/$streamId" - - val newConnection: CONNECTION - if (isUsingIPC) { - newConnection = connectionFunc(ConnectionParams(this, clientConnection, PublicKeyValidationState.VALID)) - } else { - newConnection = connectionFunc(ConnectionParams(this, clientConnection, validateRemoteAddress)) - remoteAddress!! - - // VALIDATE are we allowed to connect to this server (now that we have the initial server information) - val permitConnection = listenerManager.notifyFilter(newConnection) - if (!permitConnection) { - handshakeConnection.subscription.close() - handshakeConnection.publication.close() - - val exception = ClientRejectedException("[$aeronLogInfo - ${handshake.connectKey}] Connection (${newConnection.id}) to $remoteAddressString was not permitted!") - ListenerManager.cleanStackTrace(exception) - logger.error(exception) { "Permission error" } - throw exception + ClientRejectedException("[${handshake.connectKey}] Connection to [$addressString] has incorrect class registration details!!", e) } - logger.info { "[$aeronLogInfo - ${handshake.connectKey}] Connection (${newConnection.id}) adding new signature for $remoteAddressString : ${connectionInfo.publicKey.toHexString()}" } - storage.addRegisteredServerKey(remoteAddress!!, connectionInfo.publicKey) + exception.cleanStackTraceInternal() + listenerManager.notifyError(exception) + throw exception } + // we set up our kryo information once we connect to a server (using the server's kryo registration details) - ////////////// - /// Extra Close action - ////////////// - newConnection.closeAction = { - // this is called whenever connection.close() is called by the framework or via client.close() + // every time we connect to a server, we have to reconfigure AND reassign kryo + readKryo = serialization.newReadKryo() - // on the client, we want to GUARANTEE that the disconnect happens-before connect. - if (!lockStepForConnect.compareAndSet(null, Mutex(locked = true))) { - logger.error { "[$aeronLogInfo - ${handshake.connectKey}] Connection ${newConnection.id} : close lockStep for disconnect was in the wrong state!" } - } - isConnected = false - // this is called whenever connection.close() is called by the framework or via client.close() + /////////////// + //// CONFIG THE CLIENT + /////////////// - // make sure to call our client.notifyDisconnect() callbacks + // we are now connected, so we can connect to the NEW client-specific ports + val clientConnection = ClientConnectionDriver.build( + shutdown = shutdown, + aeronDriver = aeronDriver, + handshakeTimeoutNs = handshakeTimeoutNs, + handshakeConnection = handshakeConnection, + connectionInfo = connectionInfo, + port2Server = port2, + tagName = tag + ) + + val pubSub = clientConnection.connectionInfo + + val bufferedMessages = connectionInfo.bufferedMessages + val connectionType = if (bufferedMessages) { + "buffered connection" + } else { + "connection" + } + val connectionTypeCaps = connectionType.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } - // this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback - actionDispatch.launch { - listenerManager.notifyDisconnect(connection) - lockStepForConnect.getAndSet(null)?.unlock() - } + val logInfo = pubSub.getLogInfo(logger.isDebugEnabled) + if (logger.isDebugEnabled) { + logger.debug("Creating new $connectionType to $logInfo") + } else { + logger.info("Creating new $connectionType to $logInfo") } - // before we finish creating the connection, we initialize it (in case there needs to be logic that happens-before `onConnect` calls occur - listenerManager.notifyInit(newConnection) + val newConnection = newConnection(ConnectionParams( + publicKey = connectionInfo.publicKey, + endPoint = this, + connectionInfo = clientConnection.connectionInfo, + publicKeyValidation = validateRemoteAddress, + enableBufferedMessages = bufferedMessages, + cryptoKey = connectionInfo.secretKey + )) + + bufferedManager?.onConnect(newConnection) + + if (!handshakeConnection.pubSub.isIpc) { + // NOTE: Client can ALWAYS connect to the server. The server makes the decision if the client can connect or not. + if (logger.isTraceEnabled) { + logger.trace("[${handshakeConnection.details}] (${handshake.connectKey}) $connectionTypeCaps (${newConnection.id}) adding new signature for [$addressString -> ${connectionInfo.publicKey.toHexString()}]") + } else if (logger.isDebugEnabled) { + logger.debug("[${handshakeConnection.details}] $connectionTypeCaps (${newConnection.id}) adding new signature for [$addressString -> ${connectionInfo.publicKey.toHexString()}]") + } else if (logger.isInfoEnabled) { + logger.info("[${handshakeConnection.details}] $connectionTypeCaps adding new signature for [$addressString -> ${connectionInfo.publicKey.toHexString()}]") + } - connection0 = newConnection - addConnection(newConnection) + storage.addRegisteredServerKey(address!!, connectionInfo.publicKey) + } // tell the server our connection handshake is done, and the connection can now listen for data. // also closes the handshake (will also throw connect timeout exception) - // this value matches the server, and allows for a more robust connection attempt - val successAttemptTimeout = config.connectionCloseTimeoutInSeconds * 2 try { - handshake.done(handshakeConnection, successAttemptTimeout) + handshake.done( + endPoint = this, + handshakeConnection, clientConnection, + handshakeTimeoutNs = handshakeTimeoutNs, + logInfo = handshakeConnection.details + ) } catch (e: Exception) { - logger.error(e) { "[$aeronLogInfo - ${handshake.connectKey}] Connection (${newConnection.id}) to $remoteAddressString error during handshake" } + listenerManager.notifyError(ClientHandshakeException("[${handshakeConnection.details}] (${handshake.connectKey}) Connection (${newConnection.id}) to [$addressString] error during handshake", e)) throw e } - isConnected = true + // finished with the handshake, so always close these! + handshakeConnection.close(this) - logger.debug { "[$aeronLogInfo - ${handshake.connectKey}] Connection (${newConnection.id}) to $remoteAddressString done with handshake." } + if (logger.isTraceEnabled) { + logger.debug("[${handshakeConnection.details}] (${handshake.connectKey}) $connectionTypeCaps (${newConnection.id}) done with handshake.") + } else if (logger.isDebugEnabled) { + logger.debug("[${handshakeConnection.details}] $connectionTypeCaps (${newConnection.id}) done with handshake.") + } - // this forces the current thread to WAIT until the network poll system has started - val pollStartupLatch = CountDownLatch(1) + connection0 = newConnection + newConnection.setImage() - // have to make a new thread to listen for incoming data! - // SUBSCRIPTIONS ARE NOT THREAD SAFE! Only one thread at a time can poll them - val networkEventProcessor = Runnable { - pollStartupLatch.countDown() + // before we finish creating the connection, we initialize it (in case there needs to be logic that happens-before `onConnect` calls + listenerManager.notifyInit(newConnection) - val pollIdleStrategy = config.pollIdleStrategy.cloneToNormal() + // this enables the connection to start polling for messages + addConnection(newConnection) - while (!isShutdown()) { - if (!newConnection.isClosedViaAeron()) { - // Polls the AERON media driver subscription channel for incoming messages - val pollCount = newConnection.poll() + // if we shutdown/close before the poller starts, we don't want to block forever + pollerClosedLatch = CountDownLatch(1) + networkEventPoller.submit( + action = object : EventActionOperator { + override fun invoke(): Int { + val connection = connection0 + + // if we initiate a disconnect manually, then there is no need to wait for aeron to verify it's closed + // we only want to wait for aeron to verify it's closed if we are SUPPOSED to be connected, but there's a network blip + return if (connection != null) { + if (!shutdownEventPoller && connection.canPoll()) { + connection.poll() + } else { + // If the connection has either been closed, or has expired, it needs to be cleaned-up/deleted. + logger.error("[${connection}] connection expired (cleanup). shutdownEventPoller=$shutdownEventPoller isClosed()=${connection.isClosed()} isClosedWithTimeout=${connection.isClosedWithTimeout()}") - // 0 means we idle. >0 means reset and don't idle (because there are likely more poll events) - pollIdleStrategy.idle(pollCount) + if (logger.isDebugEnabled) { + logger.debug("[{}] connection expired (cleanup)", connection) + } + // remove ourselves from processing + EventPoller.REMOVE + } } else { - // If the connection has either been closed, or has expired, it needs to be cleaned-up/deleted. - logger.debug { "[$aeronLogInfo] connection expired" } + // remove ourselves from processing + EventPoller.REMOVE + } + } + }, + onClose = object : EventCloseOperator { + override fun invoke() { + val connection = connection0 + if (connection == null) { + logger.error("Unable to continue, as the connection has been removed before event dispatch shutdown!") + return + } - // NOTE: We do not shutdown the client!! The client is only closed by explicitly calling `client.close()` - newConnection.close() - return@Runnable + val mustRestartDriverOnError = aeronDriver.internal.mustRestartDriverOnError + val dirtyDisconnectWithSession = !shutdownEventPoller && connection.isDirtyClose() + + autoReconnect = mustRestartDriverOnError || dirtyDisconnectWithSession + + if (mustRestartDriverOnError) { + logger.error("[{}] Critical driver error detected, reconnecting client", connection) + } else if (dirtyDisconnectWithSession) { + logger.error("[{}] Dirty disconnect detected, reconnecting client", connection) + } + + // this can be closed when the connection is remotely closed in ADDITION to manually closing + if (logger.isDebugEnabled) { + logger.debug("[{}] Client event dispatch closing (in progress: $shutdownInProgress) ...", connection) + } + + // we only need to run shutdown methods if there was a network outage or D/C + if (!shutdownInProgress.value) { + // this is because we restart automatically on driver errors/weird timeouts + this@Client.close(closeEverything = false, sendDisconnectMessage = true, releaseWaitingThreads = !autoReconnect) } + + + // we can now call connect again + endpointIsRunning.lazySet(false) + pollerClosedLatch.countDown() + + + connection0 = null + + if (autoReconnect) { + // clients can reconnect automatically ONLY if there are driver errors, otherwise it's explicit! + eventDispatch.CLOSE.launch { + waitForEndpointShutdown() + + // also wait for everyone else to shutdown!! + aeronDriver.internal.endPointUsages.forEach { + if (it !== this@Client) { + it.waitForEndpointShutdown() + } + } + + // if we restart/reconnect too fast, errors from the previous run will still be present! + aeronDriver.delayLingerTimeout() + + if (connectionTimeoutSec == 0) { + logger.info("Reconnecting...") + } else { + logger.info("Reconnecting... (timeout in $connectionTimeoutSec seconds)") + } + + connect( + remoteAddress = address, + remoteAddressString = addressString, + remoteAddressPrettyString = addressPrettyString, + port1 = port1, + port2 = port2, + connectionTimeoutSec = connectionTimeoutSec, + reliable = reliable, + ) + } + } + logger.debug("[{}] Closed the Network Event Poller task.", connection) } - } - config.networkInterfaceEventDispatcher.submit(networkEventProcessor) + }) - pollStartupLatch.await() + listenerManager.notifyConnect(newConnection) - // these have to be in two SEPARATE "runnables" otherwise... - // if something inside-of listenerManager.notifyConnect is blocking or suspends, then polling will never happen! - actionDispatch.launch { - lockStepForConnect.getAndSet(null)?.withLock { } - listenerManager.notifyConnect(newConnection) - } + newConnection.sendBufferedMessages() } /** * true if the remote public key changed. This can be useful if specific actions are necessary when the key has changed. */ val remoteKeyHasChanged: Boolean - get() = connection.hasRemoteKeyChanged() + get() = connection.remoteKeyChanged /** * true if this connection is an IPC connection */ val isIPC: Boolean - get() = connection.isIpc + get() = address == null /** * @return true if this connection is a network connection @@ -851,7 +1018,7 @@ open class Client( get() = connection.isNetwork /** - * @return the connection (TCP or IPC) id of this connection. + * @return the connection id of this connection. */ val id: Int get() = connection.id @@ -873,40 +1040,58 @@ open class Client( return if (c != null) { c.send(message) + } else if (isShutdown()) { + logger.error("Cannot send a message '${message::class.java}' when there is no connection! (We are shutdown)") + false } else { - val exception = ClientException("Cannot send a message when there is no connection!") - logger.error(exception) { "No connection!" } + val exception = TransmitException("Cannot send message '${message::class.java}' when there is no connection!") + listenerManager.notifyError(exception) false } } /** - * Sends a "ping" packet to measure **ROUND TRIP** time to the remote connection. + * Safely sends objects to a destination, where the callback is notified once the remote endpoint has received the message. + * This is to guarantee happens-before, and using this will depend upon APP+NETWORK latency, and is (by design) not as performant as + * sending a regular message! * - * @param function called when the ping returns (ie: update time/latency counters/metrics/etc) - * - * @return true if the ping was successfully sent to the client + * @return true if the message was sent successfully, false if the connection has been closed */ - suspend fun ping(pingTimeoutSeconds: Int = PingManager.DEFAULT_TIMEOUT_SECONDS, function: suspend Ping.() -> Unit): Boolean { + fun send(message: Any, onSuccessCallback: CONNECTION.() -> Unit): Boolean { val c = connection0 - if (c != null) { - return pingManager.ping(c, pingTimeoutSeconds, actionDispatch, responseManager, logger, function) + return if (c != null) { + @Suppress("UNCHECKED_CAST") + c.send(message, onSuccessCallback as Connection.() -> Unit) + } else if (isShutdown()) { + logger.error("Cannot send-sync message '${message::class.java}' when there is no connection! (We are shutdown)") + false } else { - logger.error(ClientException("Cannot send a ping when there is no connection!")) { "No connection!" } + val exception = TransmitException("Cannot send-sync message '${message::class.java}' when there is no connection!") + listenerManager.notifyError(exception) + false } - - return false } /** * Sends a "ping" packet to measure **ROUND TRIP** time to the remote connection. * * @param function called when the ping returns (ie: update time/latency counters/metrics/etc) + * + * @return true if the ping was successfully sent to the client */ - fun pingBlocking(pingTimeoutSeconds: Int = PingManager.DEFAULT_TIMEOUT_SECONDS, function: suspend Ping.() -> Unit): Boolean { - return runBlocking { - ping(pingTimeoutSeconds, function) + fun ping(function: Ping.() -> Unit): Boolean { + val c = connection0 + + return if (c != null) { + c.ping(function) + } else if (isShutdown()) { + logger.error("Cannot send a ping when there is no connection! (We are shutdown)") + false + } else { + val exception = TransmitException("Cannot send a ping when there is no connection!") + listenerManager.notifyError(exception) + false } } @@ -916,12 +1101,40 @@ open class Client( fun removeRegisteredServerKey(address: InetAddress) { val savedPublicKey = storage.getRegisteredServerKey(address) if (savedPublicKey != null) { - logger.debug { "Deleting remote IP address key $address" } + if (logger.isDebugEnabled) { + logger.debug("Deleting remote IP address key $address") + } storage.removeRegisteredServerKey(address) } } - final override fun close0() { - // no impl + /** + * Will throw an exception if there are resources that are still in use + */ + fun checkForMemoryLeaks() { + AeronDriver.checkForMemoryLeaks() + } + + /** + * By default, if you call close() on the client, it will shut down all parts of the endpoint (listeners, driver, event polling, etc). + * + * @param closeEverything if true, all parts of the client will be closed (listeners, driver, event polling, etc) + */ + fun close(closeEverything: Boolean = true) { + stopConnectOnShutdown = true + bufferedManager?.close() + close(closeEverything = closeEverything, sendDisconnectMessage = true, releaseWaitingThreads = true) + } + + override fun toString(): String { + return string0 + } + + fun use(block: (Client) -> R): R { + return try { + block(this) + } finally { + close() + } } } diff --git a/src/dorkbox/network/Configuration.kt b/src/dorkbox/network/Configuration.kt index 71c13903..15335bf0 100644 --- a/src/dorkbox/network/Configuration.kt +++ b/src/dorkbox/network/Configuration.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,40 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package dorkbox.network import dorkbox.netUtil.IPv4 import dorkbox.netUtil.IPv6 -import dorkbox.network.aeron.AeronDriver -import dorkbox.network.aeron.CoroutineBackoffIdleStrategy -import dorkbox.network.aeron.CoroutineIdleStrategy -import dorkbox.network.aeron.CoroutineSleepingMillisIdleStrategy import dorkbox.network.connection.Connection +import dorkbox.network.connection.CryptoManagement import dorkbox.network.serialization.Serialization import dorkbox.os.OS import dorkbox.storage.Storage import dorkbox.util.NamedThreadFactory import io.aeron.driver.Configuration import io.aeron.driver.ThreadingMode +import io.aeron.driver.exceptions.InvalidChannelException import io.aeron.exceptions.DriverTimeoutException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import mu.KLogger -import org.agrona.SystemUtil +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.agrona.concurrent.AgentTerminationException +import org.agrona.concurrent.BackoffIdleStrategy +import org.agrona.concurrent.IdleStrategy +import org.slf4j.Logger +import org.slf4j.helpers.NOPLogger import java.io.File import java.net.BindException import java.nio.channels.ClosedByInterruptException import java.util.concurrent.* class ServerConfiguration : dorkbox.network.Configuration() { - companion object { - /** - * Gets the version number. - */ - const val version = "5.32" - } - /** * The address for the server to listen on. "*" will accept connections from all interfaces, otherwise specify * the hostname (or IP) to bind to. @@ -67,7 +61,7 @@ class ServerConfiguration : dorkbox.network.Configuration() { } /** - * The maximum number of client connection allowed per IP address. IPC is unlimited + * The maximum number of client connection allowed per IP address, Default is unlimited and IPC is always unlimited */ var maxConnectionsPerIpAddress = 0 set(value) { @@ -76,9 +70,12 @@ class ServerConfiguration : dorkbox.network.Configuration() { } /** - * The IPC ID is used to define what ID the server will receive data on. The client IPC ID must match this value. + * If a connection is in a temporal state (in the middle of a reconnect) and a buffered connection is in use -- then how long should we consider + * a new connection from the same client as part of the same "session". + * + * The session timeout cannot be shorter than 60 seconds, and the server will send this configuration to the client */ - var ipcId = AeronDriver.IPC_HANDSHAKE_STREAM_ID_SUB + var bufferedConnectionTimeoutSeconds = TimeUnit.MINUTES.toSeconds(2) set(value) { require(!contextDefined) { errorMessage } field = value @@ -93,7 +90,6 @@ class ServerConfiguration : dorkbox.network.Configuration() { field = value } - /** * Validates the current configuration */ @@ -111,33 +107,286 @@ class ServerConfiguration : dorkbox.network.Configuration() { require(listenIpAddress.isNotBlank()) { "Blank listen IP address, cannot continue." } } + + override fun initialize(logger: Logger): dorkbox.network.ServerConfiguration { + return super.initialize(logger) as dorkbox.network.ServerConfiguration + } + + override fun copy(): dorkbox.network.ServerConfiguration { + val config = ServerConfiguration() + + config.listenIpAddress = listenIpAddress + config.maxClientCount = maxClientCount + config.maxConnectionsPerIpAddress = maxConnectionsPerIpAddress + config.bufferedConnectionTimeoutSeconds = bufferedConnectionTimeoutSeconds + config.settingsStore = settingsStore + + super.copy(config) + + return config + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ServerConfiguration) return false + if (!super.equals(other)) return false + + if (listenIpAddress != other.listenIpAddress) return false + if (maxClientCount != other.maxClientCount) return false + if (maxConnectionsPerIpAddress != other.maxConnectionsPerIpAddress) return false + if (bufferedConnectionTimeoutSeconds != other.bufferedConnectionTimeoutSeconds) return false + if (settingsStore != other.settingsStore) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + listenIpAddress.hashCode() + result = 31 * result + maxClientCount + result = 31 * result + maxConnectionsPerIpAddress + result = 31 * result + bufferedConnectionTimeoutSeconds.hashCode() + result = 31 * result + settingsStore.hashCode() + return result + } } class ClientConfiguration : dorkbox.network.Configuration() { + /** - * Validates the current configuration + * Specify the UDP port to use. This port is used by the client to listen for return traffic from the server. + * + * This will normally be autoconfigured randomly to the first available port, however it can be hardcoded in the event that + * there is a reason autoconfiguration is not wanted - for example, specific firewall rules. + * + * Must be the value of an unsigned short and greater than 0 + */ + var port: Int = -1 + set(value) { + require(!contextDefined) { errorMessage } + field = value + } + + /** + * The tag name to be assigned to this connection and the server will receive this tag name during the handshake. + * The max length is 32 characters. + */ + var tag: String = "" + set(value) { + require(!contextDefined) { errorMessage } + field = value + } + + + /** + * Validates the current configuration. Throws an exception if there are problems. */ @Suppress("DuplicatedCode") override fun validate() { super.validate() + // have to do some basic validation of our configuration + if (port != -1) { + // this means it was configured! + require(port > 0) { "Client listen port must be > 0" } + require(port < 65535) { "Client listen port must be < 65535" } + } + + require(tag.length <= 32) { "Client tag name length must be <= 32" } + } + + override fun initialize(logger: Logger): dorkbox.network.ClientConfiguration { + return super.initialize(logger) as dorkbox.network.ClientConfiguration + } + + override fun copy(): dorkbox.network.ClientConfiguration { + val config = ClientConfiguration() + super.copy(config) + + config.port = port + config.tag = tag + + return config + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ClientConfiguration) return false + if (!super.equals(other)) return false + + if (port != other.port) return false + if (tag != other.tag) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + port.hashCode() + result = 31 * result + tag.hashCode() + return result } } -abstract class Configuration { +abstract class Configuration protected constructor() { + @OptIn(ExperimentalCoroutinesApi::class) companion object { + /** + * Gets the version number. + */ + const val version = "6.15" + + internal val NOP_LOGGER = NOPLogger.NOP_LOGGER + internal const val errorMessage = "Cannot set a property after the configuration context has been created!" - @Volatile - private var alreadyShownTips = false + private val appIdRegexString = Regex("a-zA-Z0-9_.-") + private val appIdRegex = Regex("^[$appIdRegexString]+$") + + internal val networkThreadGroup = ThreadGroup("Network") + internal val aeronThreadFactory = NamedThreadFactory( "Aeron", networkThreadGroup, true) + + const val UDP_HANDSHAKE_STREAM_ID: Int = 0x1337cafe // 322423550 + const val IPC_HANDSHAKE_STREAM_ID: Int = 0x1337c0de // 322420958 + + private val defaultAeronFilter: (error: Throwable) -> Boolean = { error -> + // we suppress these because they are already handled + when { + error is InvalidChannelException || error.cause is InvalidChannelException -> { false } + error is ClosedByInterruptException || error.cause is ClosedByInterruptException -> { false } + error is DriverTimeoutException || error.cause is DriverTimeoutException -> { false } + error is AgentTerminationException || error.cause is AgentTerminationException-> { false } + error is BindException || error.cause is BindException -> { false } + else -> { true } + } + } + + /** + * Determines if the app ID is valid. + */ + fun isAppIdValid(input: String): Boolean { + return appIdRegex.matches(input) + } + + /** + * Depending on the OS, different base locations for the Aeron log directory are preferred. + */ + fun defaultAeronLogLocation(logger: Logger = NOP_LOGGER): File { + return when { + OS.isMacOsX -> { + // does the recommended location exist?? + + // Default is to try the RAM drive + val suggestedLocation = File("/Volumes/DevShm") + if (suggestedLocation.exists()) { + suggestedLocation + } + else if (logger !== NOP_LOGGER) { + // don't ALWAYS create it! + + + /* + * Note: Since Mac OS does not have a built-in support for /dev/shm, we automatically create a RAM disk for the Aeron directory (aeron.dir). + * + * You can create a RAM disk with the following command: + * + * $ diskutil erasevolume APFS "DISK_NAME" `hdiutil attach -nomount ram://$((SIZE_IN_MB * 2048))` + * + * where: + * + * DISK_NAME should be replaced with a name of your choice. + * SIZE_IN_MB is the size in megabytes for the disk (e.g. 4096 for a 4GB disk). + * + * For example, the following command creates a RAM disk named DevShm which is 8GB in size: + * + * $ diskutil erasevolume APFS "DevShm" `hdiutil attach -nomount ram://$((8 * 1024 * 2048))` + * + * After this command is executed the new disk will be mounted under /Volumes/DevShm. + */ + val sizeInGB = 4 + + // on macos, we cannot rely on users to actually create this -- so we automatically do it for them. + logger.info("Creating a $sizeInGB GB RAM drive for best performance.") + + // hdiutil attach -nobrowse -nomount ram://4194304 + val newDevice = dorkbox.executor.Executor() + .command("hdiutil", "attach", "-nomount", "ram://${sizeInGB * 1024 * 2048}") + .destroyOnExit() + .enableRead() + .startBlocking(60, TimeUnit.SECONDS) + .output + .string().trim().also { if (logger.isTraceEnabled) { logger.trace("Created new disk: $it") } } + + // diskutil apfs createContainer /dev/disk4 + val lines = dorkbox.executor.Executor() + .command("diskutil", "apfs", "createContainer", newDevice) + .destroyOnExit() + .enableRead() + .startBlocking(60, TimeUnit.SECONDS) + .output + .lines().onEach { line -> logger.trace(line) } + + val newDiskLine = lines[lines.lastIndex-1] + val disk = newDiskLine.substring(newDiskLine.lastIndexOf(':')+1).trim() + + // diskutil apfs addVolume disk5 APFS DevShm -nomount + dorkbox.executor.Executor() + .command("diskutil", "apfs", "addVolume", disk, "APFS", "DevShm", "-nomount") + .destroyOnExit() + .enableRead() + .startBlocking(60, TimeUnit.SECONDS) + .output + .string().also { if (logger.isTraceEnabled) { logger.trace(it) } } + + // diskutil mount nobrowse "DevShm" + dorkbox.executor.Executor() + .command("diskutil", "mount", "nobrowse", "DevShm") + .destroyOnExit() + .enableRead() + .startBlocking(60, TimeUnit.SECONDS) + .output + .string().also { if (logger.isTraceEnabled) { logger.trace(it) } } + + // touch /Volumes/RAMDisk/.metadata_never_index + File("${suggestedLocation}/.metadata_never_index").createNewFile() + + suggestedLocation + } + else { + // we don't always want to create a ram drive! + OS.TEMP_DIR + } + } + OS.isLinux -> { + // this is significantly faster for linux than using the temp dir + File("/dev/shm/") + } + else -> { + OS.TEMP_DIR + } + } + } } + /** - * Specifies the Java thread that will poll the underlying network for incoming messages + * Specify the application ID. This is necessary, as it prevents multiple instances of aeron from responding to applications that + * is not theirs. Because of the shared nature of aeron drivers, this is necessary. + * + * This is a human-readable string, and it MUST be configured the same for both the clint/server */ - var networkInterfaceEventDispatcher: ExecutorService = Executors.newSingleThreadExecutor( - NamedThreadFactory( "Network Event Dispatcher", Thread.currentThread().threadGroup, Thread.NORM_PRIORITY, true) - ) + var appId = "" + set(value) { + require(!contextDefined) { errorMessage } + field = value + } + + /** + * In **very** unique circumstances, you might want to force the aeron driver to be shared between processes. + * + * This is only really applicable if the C driver is running/used. + */ + var forceAllowSharedAeronDriver = false set(value) { require(!contextDefined) { errorMessage } field = value @@ -174,49 +423,39 @@ abstract class Configuration { } /** - * When connecting to a remote client/server, should connections be allowed if the remote machine signature has changed? - * - * Setting this to false is not recommended as it is a security risk + * The IPC ID is used to define what ID the server will receive data on for the handshake. The client IPC ID must match this value. */ - var enableRemoteSignatureValidation: Boolean = true + var ipcId = IPC_HANDSHAKE_STREAM_ID set(value) { require(!contextDefined) { errorMessage } field = value } - /** - * Specify the UDP port to use. This port is used to establish client-server connections. - * When used for the server, this is the subscription port, which will be listening for incoming connections - * When used for the client, this is the publication port, which is what port to connect to when establishing a connection - * - * This means that client-pub -> {{network}} -> server-sub - * - * Must be the value of an unsigned short and greater than 0 + * The UDP ID is used to define what ID the server will receive data on for the handshake. The client UDP ID must match this value. */ - var port: Int = 0 + var udpId = UDP_HANDSHAKE_STREAM_ID set(value) { require(!contextDefined) { errorMessage } field = value } + /** - * How long a connection must be disconnected before we cleanup the memory associated with it + * When connecting to a remote client/server, should connections be allowed if the remote machine signature has changed? + * + * Setting this to false is not recommended as it is a security risk */ - var connectionCloseTimeoutInSeconds: Int = 10 + var enableRemoteSignatureValidation: Boolean = true set(value) { require(!contextDefined) { errorMessage } field = value } /** - * How often to check if the underlying aeron publication/subscription is connected or not. - * - * Aeron Publications and Subscriptions are, and can be, constantly in flux (because of UDP!). - * - * Too low and it's wasting CPU cycles, too high and there will be some lag when detecting if a connection has been disconnected. + * How long a connection must be disconnected before we cleanup the memory associated with it */ - var connectionCheckIntervalNanos = TimeUnit.MILLISECONDS.toNanos(200) + var connectionCloseTimeoutInSeconds: Int = 60 set(value) { require(!contextDefined) { errorMessage } field = value @@ -230,7 +469,7 @@ abstract class Configuration { * * Too low and it's likely to get false-positives, too high and there will be some lag when detecting if a connection has been disconnected. */ - var connectionExpirationTimoutNanos = TimeUnit.SECONDS.toNanos(2) + var connectionExpirationTimoutNanos = TimeUnit.SECONDS.toNanos(4) set(value) { require(!contextDefined) { errorMessage } field = value @@ -245,23 +484,6 @@ abstract class Configuration { field = value } - - /** - * The dispatch responsible for executing events that arrive via the network. - * - * This is very specifically NOT 'CoroutineScope(Dispatchers.Default)', because it is very easy (and tricky) to make sure - * that there is no thread starvation going on, which can, and WILL happen. - * - * Normally, events should be dispatched asynchronously across a thread pool, but in certain circumstances you may want to constrain this to a single thread dispatcher or other, custom dispatcher. - */ - var dispatch = CoroutineScope(Dispatchers.Default) - - /** - * Allows the user to change how endpoint settings and public key information are saved. - * - * Note: This field is overridden for server configurations, so that the file used is different for client/server - */ - /** * Allows the user to change how endpoint settings and public key information are saved. * @@ -282,6 +504,19 @@ abstract class Configuration { field = value } + /** + * What is the max stream size that can exist in memory when deciding if data chunks are in memory or on temo-file on disk. + * Data is streamed when it is too large to send in a single aeron message + * + * Must be >= 16 and <= 256 + */ + var maxStreamSizeInMemoryMB: Int = 16 + set(value) { + require(!contextDefined) { errorMessage } + field = value + } + + /** * The idle strategy used when polling the Media Driver for new messages. BackOffIdleStrategy is the DEFAULT. * @@ -293,7 +528,7 @@ abstract class Configuration { * The main difference in strategies is how responsive to changes should the idler be when idle for a little bit of time and * how much CPU should be consumed when no work is being done. There is an inherent tradeoff to consider. */ - var pollIdleStrategy: CoroutineIdleStrategy = CoroutineBackoffIdleStrategy(maxSpins = 100, maxYields = 10, minParkPeriodMs = 1, maxParkPeriodMs = 100) + var pollIdleStrategy: IdleStrategy = BackoffIdleStrategy() set(value) { require(!contextDefined) { errorMessage } field = value @@ -310,12 +545,49 @@ abstract class Configuration { * The main difference in strategies is how responsive to changes should the idler be when idle for a little bit of time and * how much CPU should be consumed when no work is being done. There is an inherent tradeoff to consider. */ - var sendIdleStrategy: CoroutineIdleStrategy = CoroutineSleepingMillisIdleStrategy(sleepPeriodMs = 100) + var sendIdleStrategy: IdleStrategy = BackoffIdleStrategy() + set(value) { + require(!contextDefined) { errorMessage } + field = value + } + + /** + * The idle strategy used by the Aeron Media Driver to write to the network when in DEDICATED mode. Null will use the aeron defaults + */ + var senderIdleStrategy: IdleStrategy? = null + set(value) { + require(!contextDefined) { errorMessage } + field = value + } + + /** + * The idle strategy used by the Aeron Media Driver read from the network when in DEDICATED mode. Null will use the aeron defaults + */ + var receiverIdleStrategy: IdleStrategy? = null + set(value) { + require(!contextDefined) { errorMessage } + field = value + } + + /** + * The idle strategy used by the Aeron Media Driver to read/write to the network when in NETWORK_SHARED mode. Null will use the aeron defaults + */ + var sharedIdleStrategy: IdleStrategy? = null set(value) { require(!contextDefined) { errorMessage } field = value } + /** + * The idle strategy used by the Aeron Media Driver conductor when in DEDICATED mode. Null will use the aeron defaults + */ + var conductorIdleStrategy: IdleStrategy? = null + set(value) { + require(!contextDefined) { errorMessage } + field = value + } + + /** * ## A Media Driver, whether being run embedded or not, needs 1-3 threads to perform its operation. * @@ -340,7 +612,7 @@ abstract class Configuration { * SHARED_NETWORK or SHARED. INVOKER can be used for low resource environments while the application using Aeron can invoke the * media driver to carry out its duty cycle on a regular interval. */ - var threadingMode = ThreadingMode.SHARED + var threadingMode = ThreadingMode.SHARED_NETWORK set(value) { require(!contextDefined) { errorMessage } field = value @@ -352,9 +624,12 @@ abstract class Configuration { var aeronDirectory: File? = null set(value) { require(!contextDefined) { errorMessage } - field = value + field = value?.absoluteFile ?: value } + + internal var uniqueAeronDirectoryID = 0 + /** * Should we force the Aeron location to be unique for every instance? This is mutually exclusive with IPC. */ @@ -372,10 +647,8 @@ abstract class Configuration { * (> 4KB) messages and for maximizing throughput above everything else. Various checks during publication and subscription/connection * setup are done to verify a decent relationship with MTU. * - * * However, it is good to understand these relationships. * - * * The MTU on the Media Driver controls the length of the MTU of data frames. This value is communicated to the Aeron clients during * registration. So, applications do not have to concern themselves with the MTU value used by the Media Driver and use the same value. * @@ -383,13 +656,9 @@ abstract class Configuration { * An MTU value over the interface MTU will cause IP to fragment the datagram. This may increase the likelihood of loss under several * circumstances. If increasing the MTU over the interface MTU, consider various ways to increase the interface MTU first in preparation. * - * * The MTU value indicates the largest message that Aeron will send as a single data frame. - * - * * MTU length also has implications for socket buffer sizing. * - * * Default value is 1408 for internet; for a LAN, 9k is possible with jumbo frames (if the routers/interfaces support it) */ var networkMtuSize = Configuration.MTU_LENGTH_DEFAULT @@ -398,6 +667,12 @@ abstract class Configuration { field = value } + var ipcMtuSize = Configuration.MAX_UDP_PAYLOAD_LENGTH + set(value) { + require(!contextDefined) { errorMessage } + field = value + } + /** * Default initial window length for flow control sender to receiver purposes. This assumes a system free of pauses. * @@ -411,7 +686,7 @@ abstract class Configuration { * Buffer (10 Gps) = (10 * 1000 * 1000 * 1000 / 8) * 0.0001 = 125000 (Round to 128KB) * Buffer (1 Gps) = (1 * 1000 * 1000 * 1000 / 8) * 0.0001 = 12500 (Round to 16KB) */ - var initialWindowLength = SystemUtil.getSizeAsInt(Configuration.INITIAL_WINDOW_LENGTH_PROP_NAME, 16 * 1024) + var initialWindowLength = 16 * 1024 set(value) { require(!contextDefined) { errorMessage } field = value @@ -430,7 +705,7 @@ abstract class Configuration { * * A value of 0 will 'auto-configure' this setting */ - var sendBufferSize = 2097152 + var sendBufferSize = 0 set(value) { require(!contextDefined) { errorMessage } field = value @@ -447,7 +722,47 @@ abstract class Configuration { * * A value of 0 will 'auto-configure' this setting. */ - var receiveBufferSize = 2097152 + var receiveBufferSize = 0 + set(value) { + require(!contextDefined) { errorMessage } + field = value + } + + + /** + * The "term" buffer is the size of EACH of the 3 (Active, Dirty, Clean) log files that are used to send/receive messages. + * - the smallest term buffer length is 65536 bytes; + * - the largest term buffer length is 1,073,741,824 bytes; + * - the size must be a power of 2; + * - maximum message length is the smallest value of 16 megabytes or (term buffer length) / 8; + * + * A value of 0 will 'auto-configure' this setting to use the default of 64 megs. Be wary, it is easy to run out of space w/ lots of clients + * + * @see [io.aeron.driver.Configuration.TERM_BUFFER_IPC_LENGTH_DEFAULT] + * @see [io.aeron.logbuffer.LogBufferDescriptor.TERM_MIN_LENGTH] + * @see [io.aeron.logbuffer.LogBufferDescriptor.TERM_MAX_LENGTH] + */ + var ipcTermBufferLength = 8 * 1024 * 1024 + set(value) { + require(!contextDefined) { errorMessage } + field = value + } + + + /** + * The "term" buffer is the size of EACH of the 3 (Active, Dirty, Clean) log files that are used to send/receive messages. + * - the smallest term buffer length is 65536 bytes; + * - the largest term buffer length is 1,073,741,824 bytes; + * - the size must be a power of 2; + * - maximum message length is the smallest value of 16 megabytes or (term buffer length) / 8; + * + * A value of 0 will 'auto-configure' this setting to use the default of 16 megs. Be wary, it is easy to run out of space w/ lots of clients + * + * @see [io.aeron.driver.Configuration.TERM_BUFFER_LENGTH_DEFAULT] + * @see [io.aeron.logbuffer.LogBufferDescriptor.TERM_MIN_LENGTH] + * @see [io.aeron.logbuffer.LogBufferDescriptor.TERM_MAX_LENGTH] + */ + var publicationTermBufferLength = 2 * 1024 * 1024 set(value) { require(!contextDefined) { errorMessage } field = value @@ -461,77 +776,37 @@ abstract class Configuration { * the emitted errors from Aeron when we attempt/retry connections. This filters out those errors so we can log (or perform an action) * when those errors are encountered * - * This is for advanced usage, and REALLY should never be over-riden. + * This is for advanced usage, and REALLY should never be over-ridden. * * @return true if the error message should be logged, false to suppress the error */ - var aeronErrorFilter: (error: Throwable) -> Boolean = { error -> - // we suppress these because they are already handled - when { - error is ClosedByInterruptException || error.cause is ClosedByInterruptException -> { false } - error is DriverTimeoutException || error.cause is DriverTimeoutException -> { false } - error is AgentTerminationException || error.cause is AgentTerminationException-> { false } - error is BindException || error.cause is BindException -> { false } - else -> { true } - } - } + var aeronErrorFilter: (error: Throwable) -> Boolean = defaultAeronFilter set(value) { require(!contextDefined) { errorMessage } field = value } - /** * Internal property that tells us if this configuration has already been configured and used to create and start the Media Driver */ @Volatile internal var contextDefined: Boolean = false - /** - * Internal property that tells us if this configuration has already been used in an endpoint - */ - @Volatile - internal var previouslyUsed = false - - /** - * Depending on the OS, different base locations for the Aeron log directory are preferred. - */ - fun suggestAeronLogLocation(logger: KLogger): File { - return when { - OS.isMacOsX -> { - // does the recommended location exist?? - - // Default is to try the RAM drive - val suggestedLocation = File("/Volumes/DevShm") - if (suggestedLocation.exists()) { - suggestedLocation - } - else { - if (!alreadyShownTips) { - alreadyShownTips = true - logger.info("It is recommended to create a RAM drive for best performance. For example\n" + "\$ diskutil erasevolume HFS+ \"DevShm\" `hdiutil attach -nomount ram://\$((2048 * 2048))`") - } - - OS.TEMP_DIR - } - } - OS.isLinux -> { - // this is significantly faster for linux than using the temp dir - File("/dev/shm/") - } - else -> { - OS.TEMP_DIR - } - } - } /** - * Validates the current configuration + * Validates the current configuration. Throws an exception if there are problems. */ @Suppress("DuplicatedCode") open fun validate() { // have to do some basic validation of our configuration + require(appId.isNotEmpty()) { "The application ID must be set, as it prevents an listener from responding to differently configured applications. This is a human-readable string, and it MUST be configured the same for both the clint/server!"} + + // The applicationID is used to create the prefix for the aeron directory -- EVEN IF the directory name is specified. + require(appId.length < 32) { "The application ID is too long, it must be < 32 characters" } + + require(isAppIdValid(appId)) { "The application ID is not valid. It may only be the following characters: $appIdRegexString" } + // can't disable everything! require(enableIpc || enableIPv4 || enableIPv6) { "At least one of IPC/IPv4/IPv6 must be enabled!" } @@ -547,10 +822,336 @@ abstract class Configuration { } } - require(port > 0) { "configuration controlPort must be > 0" } - require(port < 65535) { "configuration controlPort must be < 65535" } + require(maxStreamSizeInMemoryMB >= 16) { "configuration maxStreamSizeInMemoryMB must be >= 16" } + require(maxStreamSizeInMemoryMB <= 256) { "configuration maxStreamSizeInMemoryMB must be <= 256" } // 256 is arbitrary require(networkMtuSize > 0) { "configuration networkMtuSize must be > 0" } - require(networkMtuSize < 9 * 1024) { "configuration networkMtuSize must be < ${9 * 1024}" } + require(networkMtuSize < Configuration.MAX_UDP_PAYLOAD_LENGTH) { "configuration networkMtuSize must be < ${Configuration.MAX_UDP_PAYLOAD_LENGTH}" } + require(ipcMtuSize > 0) { "configuration ipcMtuSize must be > 0" } + require(ipcMtuSize <= Configuration.MAX_UDP_PAYLOAD_LENGTH) { "configuration ipcMtuSize must be <= ${Configuration.MAX_UDP_PAYLOAD_LENGTH}" } + + require(sendBufferSize >= 0) { "configuration socket send buffer must be >= 0"} + require(receiveBufferSize >= 0) { "configuration socket receive buffer must be >= 0"} + require(ipcTermBufferLength > 65535) { "configuration IPC term buffer must be > 65535"} + require(ipcTermBufferLength < 1_073_741_824) { "configuration IPC term buffer must be < 1,073,741,824"} + require(publicationTermBufferLength > 65535) { "configuration publication term buffer must be > 65535"} + require(publicationTermBufferLength < 1_073_741_824) { "configuration publication term buffer must be < 1,073,741,824"} + } + + internal open fun initialize(logger: Logger): dorkbox.network.Configuration { + // explicitly don't set defaults if we already have the context defined! + if (contextDefined) { + return this + } + + // we are starting a new context, make sure the aeron directory is unique (if specified) + if (uniqueAeronDirectory) { + uniqueAeronDirectoryID = CryptoManagement.secureRandom.let { + // make sure it's not 0, because 0 is special + var id = 0 + while (id == 0) id = it.nextInt() + id + } + } + +// /* +// * Linux +// * Linux normally requires some settings of sysctl values. One is net.core.rmem_max to allow larger SO_RCVBUF and +// * net.core.wmem_max to allow larger SO_SNDBUF values to be set. +// * +// * Windows +// * Windows tends to use SO_SNDBUF values that are too small. It is recommended to use values more like 1MB or so. +// * +// * Mac/Darwin +// * Mac tends to use SO_SNDBUF values that are too small. It is recommended to use larger values, like 16KB. +// */ +// if (receiveBufferSize == 0) { +// receiveBufferSize = io.aeron.driver.Configuration.SOCKET_RCVBUF_LENGTH_DEFAULT * 4 +// // when { +// // OS.isLinux() -> +// // OS.isWindows() -> +// // OS.isMacOsX() -> +// // } +// +// // val rmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.rmem_max") +// } +// +// +// if (sendBufferSize == 0) { +// sendBufferSize = io.aeron.driver.Configuration.SOCKET_SNDBUF_LENGTH_DEFAULT * 4 +// // when { +// // OS.isLinux() -> +// // OS.isWindows() -> +// // OS.isMacOsX() -> +// // } +// +// val wmem_max = dorkbox.netUtil.SocketUtils.sysctlGetInt("net.core.wmem_max") +// } + + + var dir = aeronDirectory + + if (dir != null) { + if (forceAllowSharedAeronDriver) { + logger.warn("Forcing the Aeron driver to be shared between processes. THIS IS DANGEROUS!") + } else if (!dir.absolutePath.endsWith(appId)) { + // we have defined an aeron directory + dir = File(dir.absolutePath + "_$appId") + } + } + else { + val baseFileLocation = defaultAeronLogLocation(logger) + val prefix = if (appId.startsWith("aeron_")) { + "" + } else { + "aeron_" + } + + + val aeronLogDirectory = if (uniqueAeronDirectory) { + // this is incompatible with IPC, and will not be set if IPC is enabled (error will be thrown on validate) + File(baseFileLocation, "$prefix${appId}_${mediaDriverIdNoDir()}") + } else { + File(baseFileLocation, "$prefix$appId") + } + dir = aeronLogDirectory.absoluteFile + } + + aeronDirectory = dir!!.absoluteFile + + // cannot make any more changes to the configuration! + contextDefined = true + + return this + } + + + // internal class for making sure that the AeronDriver is not duplicated for the same configuration (as that is entirely unnecessary) + internal class MediaDriverConfig(val config: dorkbox.network.Configuration) { + val connectionCloseTimeoutInSeconds get() = config.connectionCloseTimeoutInSeconds + val threadingMode get() = config.threadingMode + + val networkMtuSize get() = config.networkMtuSize + val ipcMtuSize get() = config.ipcMtuSize + val initialWindowLength get() = config.initialWindowLength + val sendBufferSize get() = config.sendBufferSize + val receiveBufferSize get() = config.receiveBufferSize + + val aeronDirectory get() = config.aeronDirectory + val uniqueAeronDirectory get() = config.uniqueAeronDirectory + val uniqueAeronDirectoryID get() = config.uniqueAeronDirectoryID + + val forceAllowSharedAeronDriver get() = config.forceAllowSharedAeronDriver + + val ipcTermBufferLength get() = config.ipcTermBufferLength + val publicationTermBufferLength get() = config.publicationTermBufferLength + + val conductorIdleStrategy get() = config.conductorIdleStrategy + val sharedIdleStrategy get() = config.sharedIdleStrategy + val receiverIdleStrategy get() = config.receiverIdleStrategy + val senderIdleStrategy get() = config.senderIdleStrategy + + val aeronErrorFilter get() = config.aeronErrorFilter + var contextDefined + get() = config.contextDefined + set(value) { + config.contextDefined = value + } + + /** + * Validates the current configuration. Throws an exception if there are problems. + */ + @Suppress("DuplicatedCode") + fun validate() { + // already validated! do nothing. + } + + /** + * Normally, the hashCode MAY be duplicate for entities that are similar, but not the same identity (via .equals). In the case + * of the MediaDriver config, the ID is used to uniquely identify a config that has the same VALUES, but is not the same REFERENCE. + * + * This is because configs that are DIFFERENT, but have the same values MUST use the same aeron driver. + */ + fun mediaDriverId(): Int { + return config.mediaDriverId() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is MediaDriverConfig) return false + + return mediaDriverEquals(config) + } + + override fun hashCode(): Int { + return config.mediaDriverId() + } + + @Suppress("DuplicatedCode", "RedundantIf") + private fun mediaDriverEquals(other: dorkbox.network.Configuration): Boolean { + if (forceAllowSharedAeronDriver != other.forceAllowSharedAeronDriver) return false + if (connectionCloseTimeoutInSeconds != other.connectionCloseTimeoutInSeconds) return false + if (threadingMode != other.threadingMode) return false + if (networkMtuSize != other.networkMtuSize) return false + if (ipcMtuSize != other.ipcMtuSize) return false + if (initialWindowLength != other.initialWindowLength) return false + if (sendBufferSize != other.sendBufferSize) return false + if (receiveBufferSize != other.receiveBufferSize) return false + + if (aeronDirectory != other.aeronDirectory) return false + if (uniqueAeronDirectory != other.uniqueAeronDirectory) return false + if (uniqueAeronDirectoryID != other.uniqueAeronDirectoryID) return false + + if (ipcTermBufferLength != other.ipcTermBufferLength) return false + if (publicationTermBufferLength != other.publicationTermBufferLength) return false + if (aeronErrorFilter != other.aeronErrorFilter) return false + + if (conductorIdleStrategy != other.conductorIdleStrategy) return false + if (sharedIdleStrategy != other.sharedIdleStrategy) return false + if (receiverIdleStrategy != other.receiverIdleStrategy) return false + if (senderIdleStrategy != other.senderIdleStrategy) return false + + return true + } + } + + @Suppress("DuplicatedCode", "RedundantIf") + private fun mediaDriverEquals(other: dorkbox.network.Configuration): Boolean { + if (forceAllowSharedAeronDriver != other.forceAllowSharedAeronDriver) return false + if (threadingMode != other.threadingMode) return false + if (networkMtuSize != other.networkMtuSize) return false + if (ipcMtuSize != other.ipcMtuSize) return false + if (initialWindowLength != other.initialWindowLength) return false + if (sendBufferSize != other.sendBufferSize) return false + if (receiveBufferSize != other.receiveBufferSize) return false + + if (conductorIdleStrategy != other.conductorIdleStrategy) return false + if (sharedIdleStrategy != other.sharedIdleStrategy) return false + if (receiverIdleStrategy != other.receiverIdleStrategy) return false + if (senderIdleStrategy != other.senderIdleStrategy) return false + + if (aeronDirectory != other.aeronDirectory) return false + if (uniqueAeronDirectory != other.uniqueAeronDirectory) return false + if (uniqueAeronDirectoryID != other.uniqueAeronDirectoryID) return false + + if (ipcTermBufferLength != other.ipcTermBufferLength) return false + if (publicationTermBufferLength != other.publicationTermBufferLength) return false + if (aeronErrorFilter != other.aeronErrorFilter) return false + + return true + } + + abstract fun copy(): dorkbox.network.Configuration + protected fun copy(config: dorkbox.network.Configuration) { + config.appId = appId + config.forceAllowSharedAeronDriver = forceAllowSharedAeronDriver + config.enableIPv4 = enableIPv4 + config.enableIPv6 = enableIPv6 + config.enableIpc = enableIpc + config.ipcId = ipcId + config.udpId = udpId + config.enableRemoteSignatureValidation = enableRemoteSignatureValidation + config.connectionCloseTimeoutInSeconds = connectionCloseTimeoutInSeconds + config.connectionExpirationTimoutNanos = connectionExpirationTimoutNanos + config.isReliable = isReliable + config.settingsStore = settingsStore + config.serialization = serialization + config.maxStreamSizeInMemoryMB = maxStreamSizeInMemoryMB + config.pollIdleStrategy = pollIdleStrategy + config.sendIdleStrategy = sendIdleStrategy + config.threadingMode = threadingMode + config.aeronDirectory = aeronDirectory + config.uniqueAeronDirectoryID = uniqueAeronDirectoryID + config.uniqueAeronDirectory = uniqueAeronDirectory + config.networkMtuSize = networkMtuSize + config.initialWindowLength = initialWindowLength + config.sendBufferSize = sendBufferSize + config.receiveBufferSize = receiveBufferSize + config.ipcTermBufferLength = ipcTermBufferLength + config.publicationTermBufferLength = publicationTermBufferLength + config.aeronErrorFilter = aeronErrorFilter + // config.contextDefined = contextDefined // we want to be able to reuse this config if it's a copy of an already defined config + } + + fun mediaDriverId(): Int { + var result = mediaDriverIdNoDir() + result = 31 * result + aeronDirectory.hashCode() + + return result + } + + private fun mediaDriverIdNoDir(): Int { + var result = threadingMode.hashCode() + result = 31 * result + networkMtuSize + result = 31 * result + ipcMtuSize + result = 31 * result + initialWindowLength + result = 31 * result + sendBufferSize + result = 31 * result + receiveBufferSize + + result = 31 * result + uniqueAeronDirectoryID + result = 31 * result + uniqueAeronDirectory.hashCode() + + result = 31 * result + ipcTermBufferLength + result = 31 * result + publicationTermBufferLength + + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is dorkbox.network.Configuration) return false + + // some values are defined here. Not necessary to list them twice + if (!mediaDriverEquals(other)) return false + + if (appId != other.appId) return false + if (enableIPv4 != other.enableIPv4) return false + if (enableIPv6 != other.enableIPv6) return false + if (enableIpc != other.enableIpc) return false + if (ipcId != other.ipcId) return false + if (udpId != other.udpId) return false + + if (enableRemoteSignatureValidation != other.enableRemoteSignatureValidation) return false + if (connectionCloseTimeoutInSeconds != other.connectionCloseTimeoutInSeconds) return false + if (connectionExpirationTimoutNanos != other.connectionExpirationTimoutNanos) return false + + if (isReliable != other.isReliable) return false + if (settingsStore != other.settingsStore) return false + if (serialization != other.serialization) return false + if (maxStreamSizeInMemoryMB != other.maxStreamSizeInMemoryMB) return false + + if (pollIdleStrategy != other.pollIdleStrategy) return false + if (sendIdleStrategy != other.sendIdleStrategy) return false + + if (uniqueAeronDirectory != other.uniqueAeronDirectory) return false + if (uniqueAeronDirectoryID != other.uniqueAeronDirectoryID) return false + if (ipcTermBufferLength != other.ipcTermBufferLength) return false + if (contextDefined != other.contextDefined) return false + + return true + } + + override fun hashCode(): Int { + var result = mediaDriverId() + + result = 31 * result + appId.hashCode() + result = 31 * result + forceAllowSharedAeronDriver.hashCode() + result = 31 * result + enableIPv4.hashCode() + result = 31 * result + enableIPv6.hashCode() + result = 31 * result + enableIpc.hashCode() + result = 31 * result + enableRemoteSignatureValidation.hashCode() + result = 31 * result + ipcId + result = 31 * result + udpId + result = 31 * result + connectionCloseTimeoutInSeconds + result = 31 * result + connectionExpirationTimoutNanos.hashCode() + result = 31 * result + isReliable.hashCode() + result = 31 * result + settingsStore.hashCode() + result = 31 * result + serialization.hashCode() + result = 31 * result + maxStreamSizeInMemoryMB + result = 31 * result + pollIdleStrategy.hashCode() + result = 31 * result + sendIdleStrategy.hashCode() + // aeronErrorFilter // cannot get the predictable hash code of a lambda + result = 31 * result + contextDefined.hashCode() + return result } } diff --git a/src/dorkbox/network/Server.kt b/src/dorkbox/network/Server.kt index 883171c9..4efe2ad7 100644 --- a/src/dorkbox/network/Server.kt +++ b/src/dorkbox/network/Server.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2024 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,25 +15,18 @@ */ package dorkbox.network -import dorkbox.netUtil.IPv4 -import dorkbox.netUtil.IPv6 -import dorkbox.netUtil.Inet4 -import dorkbox.netUtil.Inet6 -import dorkbox.network.aeron.AeronDriver -import dorkbox.network.aeron.AeronPoller -import dorkbox.network.connection.Connection -import dorkbox.network.connection.ConnectionParams -import dorkbox.network.connection.EndPoint -import dorkbox.network.connectionType.ConnectionRule -import dorkbox.network.exceptions.AllocationException +import dorkbox.hex.toHexString +import dorkbox.network.aeron.* +import dorkbox.network.connection.* +import dorkbox.network.connection.IpInfo.Companion.IpListenType +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace +import dorkbox.network.connection.buffer.BufferManager import dorkbox.network.exceptions.ServerException import dorkbox.network.handshake.ServerHandshake import dorkbox.network.handshake.ServerHandshakePollers +import dorkbox.network.ipFilter.IpFilterRule import dorkbox.network.rmi.RmiSupportServer -import kotlinx.atomicfu.atomic -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import org.slf4j.LoggerFactory import java.net.InetAddress import java.util.concurrent.* @@ -47,90 +40,41 @@ import java.util.concurrent.* * @param connectionFunc allows for custom connection implementations defined as a unit function * @param loggerName allows for a custom logger name for this endpoint (for when there are multiple endpoints) */ -open class Server( - config: ServerConfiguration = ServerConfiguration(), - connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION, - loggerName: String = Server::class.java.simpleName) - : EndPoint(config, connectionFunc, loggerName) { - - /** - * The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the - * server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections()) - * - * To put it bluntly, ONLY have the server do work inside a listener! - * - * @param config these are the specific connection options - * @param loggerName allows for a custom logger name for this endpoint (for when there are multiple endpoints) - * @param connectionFunc allows for custom connection implementations defined as a unit function - */ - constructor(config: ServerConfiguration, - loggerName: String, - connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION) - : this(config, connectionFunc, loggerName) - - - /** - * The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the - * server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections()) - * - * To put it bluntly, ONLY have the server do work inside of a listener! - * - * @param config these are the specific connection options - * @param connectionFunc allows for custom connection implementations defined as a unit function - */ - constructor(config: ServerConfiguration, - connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION) - : this(config, connectionFunc, Server::class.java.simpleName) - - - /** - * The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the - * server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections()) - * - * To put it bluntly, ONLY have the server do work inside of a listener! - * - * @param config these are the specific connection options - * @param loggerName allows for a custom logger name for this endpoint (for when there are multiple endpoints) - */ - constructor(config: ServerConfiguration, - loggerName: String = Server::class.java.simpleName) - : this(config, - { - @Suppress("UNCHECKED_CAST") - Connection(it) as CONNECTION - }, - loggerName) - - /** - * The server can only be accessed in an ASYNC manner. This means that the server can only be used in RESPONSE to events. If you access the - * server OUTSIDE of events, you will get inaccurate information from the server (such as getConnections()) - * - * To put it bluntly, ONLY have the server do work inside of a listener! - * - * @param config these are the specific connection options - */ - constructor(config: ServerConfiguration) - : this(config, - { - @Suppress("UNCHECKED_CAST") - Connection(it) as CONNECTION - }, - Server::class.java.simpleName) - +open class Server(config: ServerConfiguration = ServerConfiguration(), loggerName: String = Server::class.java.simpleName) + : EndPoint(config, loggerName) { companion object { /** * Gets the version number. */ - const val version = "5.32" + const val version = Configuration.version + + /** + * Ensures that an endpoint (using the specified configuration) is NO LONGER running. + * + * NOTE: This method should only be used to check if a server is running for a DIFFERENT configuration than the currently running server + * + * By default, we will wait the [Configuration.connectionCloseTimeoutInSeconds] * 2 amount of time before returning. + * + * @return true if the media driver is STOPPED. + */ + fun ensureStopped(configuration: ServerConfiguration): Boolean { + val timeout = TimeUnit.SECONDS.toMillis(configuration.connectionCloseTimeoutInSeconds.toLong() * 2) + + val logger = LoggerFactory.getLogger(Server::class.java.simpleName) + return AeronDriver.ensureStopped(configuration.copy(), logger, timeout) + } /** * Checks to see if a server (using the specified configuration) is running. * - * This method should only be used to check if a server is running for a DIFFERENT configuration than the currently running server + * NOTE: This method should only be used to check if a server is running for a DIFFERENT configuration than the currently running server + * + * @return true if the media driver is active and running */ fun isRunning(configuration: ServerConfiguration): Boolean { - return AeronDriver(configuration).isRunning() + val logger = LoggerFactory.getLogger(Server::class.java.simpleName) + return AeronDriver.isRunning(configuration.copy(), logger) } init { @@ -144,261 +88,310 @@ open class Server( */ val rmiGlobal = RmiSupportServer(logger, rmiGlobalSupport) - /** - * @return true if this server has successfully bound to an IP address and is running - */ - private var bindAlreadyCalled = atomic(false) +// /** +// * Maintains a thread-safe collection of rules used to define the connection type with this server. +// */ +// private val connectionRules = CopyOnWriteArrayList() /** - * These are run in lock-step to shutdown/close the server. Afterwards, bind() can be called again + * the IP address information, if available. */ - @Volatile - private var shutdownPollLatch = CountDownLatch(1) + internal val ipInfo = IpInfo(config) @Volatile - private var shutdownEventLatch = CountDownLatch(1) + internal lateinit var handshake: ServerHandshake /** - * Maintains a thread-safe collection of rules used to define the connection type with this server. + * Different connections (to the same client) can be "buffered", meaning that if they "go down" because of a network glitch -- the data + * being sent is not lost (it is buffered) and then re-sent once the new connection is established. References to the old connection + * will also redirect to the new connection. */ - private val connectionRules = CopyOnWriteArrayList() + internal val bufferedManager: BufferManager - /** - * true if the following network stacks are available for use - */ - internal val canUseIPv4 = config.enableIPv4 && IPv4.isAvailable - internal val canUseIPv6 = config.enableIPv6 && IPv6.isAvailable - - - // localhost/loopback IP might not always be 127.0.0.1 or ::1 - // We want to listen on BOTH IPv4 and IPv6 (config option lets us configure this) - internal val listenIPv4Address: InetAddress? = - if (canUseIPv4) { - when (config.listenIpAddress) { - "loopback", "localhost", "lo", "127.0.0.1", "::1" -> IPv4.LOCALHOST - "0", "::", "0.0.0.0", "*" -> { - // this is the "wildcard" address. Windows has problems with this. - IPv4.WILDCARD - } - else -> Inet4.toAddress(config.listenIpAddress) // Inet4Address.getAllByName(config.listenIpAddress)[0] - } - } - else { - null - } - - - internal val listenIPv6Address: InetAddress? = - if (canUseIPv6) { - when (config.listenIpAddress) { - "loopback", "localhost", "lo", "127.0.0.1", "::1" -> IPv6.LOCALHOST - "0", "::", "0.0.0.0", "*" -> { - // this is the "wildcard" address. Windows has problems with this. - IPv6.WILDCARD - } - else -> Inet6.toAddress(config.listenIpAddress) - } - } - else { - null - } + private val string0: String by lazy { + "EndPoint [Server: ${storage.publicKey.toHexString()}]" + } init { - // we are done with initial configuration, now finish serialization - serialization.finishInit(type) + bufferedManager = BufferManager(config, listenerManager, aeronDriver, config.bufferedConnectionTimeoutSeconds) } final override fun newException(message: String, cause: Throwable?): Throwable { - return ServerException(message, cause) + // +2 because we do not want to see the stack for the abstract `newException` + val serverException = ServerException(message, cause) + serverException.cleanStackTrace(2) + return serverException + } + + /** + * Binds the server IPC only, using the previously set AERON configuration + */ + fun bindIpc() { + if (!config.enableIpc) { + logger.warn("IPC explicitly requested, but not enabled. Enabling IPC...") + // we explicitly requested IPC, make sure it's enabled + config.contextDefined = false + config.enableIpc = true + config.contextDefined = true + } + + if (config.enableIPv4) { logger.warn("IPv4 is enabled, but only IPC will be used.") } + if (config.enableIPv6) { logger.warn("IPv6 is enabled, but only IPC will be used.") } + + internalBind(port1 = 0, port2 = 0, onlyBindIpc = true, runShutdownCheck = true) } /** - * Binds the server to AERON configuration + * Binds the server to UDP ports, using the previously set AERON configuration + * + * @param port1 this is the network port which will be listening for incoming connections + * @param port2 this is the network port that the server will use to work around NAT firewalls. By default, this is port1+1, but + * can also be configured independently. This is required, and must be different from port1. */ @Suppress("DuplicatedCode") - fun bind() { - // NOTE: it is critical to remember that Aeron DOES NOT like running from coroutines! + fun bind(port1: Int, port2: Int = port1+1) { + if (config.enableIPv4 || config.enableIPv6) { + require(port1 != port2) { "port1 cannot be the same as port2" } + require(port1 > 0) { "port1 must be > 0" } + require(port2 > 0) { "port2 must be > 0" } + require(port1 < 65535) { "port1 must be < 65535" } + require(port2 < 65535) { "port2 must be < 65535" } + } - if (bindAlreadyCalled.getAndSet(true)) { - logger.error { "Unable to bind when the server is already running!" } + internalBind(port1 = port1, port2 = port2, onlyBindIpc = false, runShutdownCheck = true) + } + + @Suppress("DuplicatedCode") + private fun internalBind(port1: Int, port2: Int, onlyBindIpc: Boolean, runShutdownCheck: Boolean) { + // the lifecycle of a server is the ENDPOINT (measured via the network event poller) + if (endpointIsRunning.value) { + listenerManager.notifyError(ServerException("Unable to start, the server is already running!")) + return + } + + if (runShutdownCheck && !waitForEndpointShutdown()) { + listenerManager.notifyError(ServerException("Unable to start the server!")) return } try { startDriver() - } catch (e: Exception) { - logger.error(e) { "Unable to start the network driver" } + initializeState() + } + catch (e: Exception) { + resetOnError() + listenerManager.notifyError(ServerException("Unable to start the server!", e)) return } - shutdownPollLatch = CountDownLatch(1) - shutdownEventLatch = CountDownLatch(1) + this@Server.port1 = port1 + this@Server.port2 = port2 config as ServerConfiguration - val handshake = ServerHandshake(logger, config, listenerManager, aeronDriver) - // we are done with initial configuration, now initialize aeron and the general state of this endpoint - // this forces the current thread to WAIT until poll system has started - val pollStartupLatch = CountDownLatch(1) - val server = this@Server - val ipcPoller: AeronPoller = ServerHandshakePollers.ipc(aeronDriver, config, server, handshake) + handshake = ServerHandshake(config, listenerManager, aeronDriver, eventDispatch) - // if we are binding to WILDCARD, then we have to do something special if BOTH IPv4 and IPv6 are enabled! - val isWildcard = listenIPv4Address == IPv4.WILDCARD || listenIPv6Address == IPv6.WILDCARD - val ipv4Poller: AeronPoller - val ipv6Poller: AeronPoller - - if (isWildcard) { - if (canUseIPv4 && canUseIPv6) { - // IPv6 will bind to IPv4 wildcard as well, so don't bind both! - ipv4Poller = ServerHandshakePollers.disabled("IPv4 Disabled") - ipv6Poller = ServerHandshakePollers.ip6Wildcard(aeronDriver, config, server, handshake) - } else { - // only 1 will be a real poller - ipv4Poller = ServerHandshakePollers.ip4(aeronDriver, config, server, handshake) - ipv6Poller = ServerHandshakePollers.ip6(aeronDriver, config, server, handshake) - } + val ipcPoller: AeronPoller = if (config.enableIpc || onlyBindIpc) { + ServerHandshakePollers.ipc(server, handshake) } else { - ipv4Poller = ServerHandshakePollers.ip4(aeronDriver, config, server, handshake) - ipv6Poller = ServerHandshakePollers.ip6(aeronDriver, config, server, handshake) + ServerHandshakePollers.disabled("IPC Disabled") } - val networkEventProcessor = Runnable { - pollStartupLatch.countDown() + val ipPoller = if (onlyBindIpc) { + ServerHandshakePollers.disabled("IPv4/6 Disabled") + } else { + when (ipInfo.ipType) { + // IPv6 will bind to IPv4 wildcard as well, so don't bind both! + IpListenType.IPWildcard -> ServerHandshakePollers.ip6Wildcard(server, handshake) + IpListenType.IPv4Wildcard -> ServerHandshakePollers.ip4(server, handshake) + IpListenType.IPv6Wildcard -> ServerHandshakePollers.ip6(server, handshake) + IpListenType.IPv4 -> ServerHandshakePollers.ip4(server, handshake) + IpListenType.IPv6 -> ServerHandshakePollers.ip6(server, handshake) + IpListenType.IPC -> ServerHandshakePollers.disabled("IPv4/6 Disabled") + } + } - val pollIdleStrategy = config.pollIdleStrategy.cloneToNormal() - try { - var pollCount: Int - while (!isShutdown()) { - pollCount = 0 + logger.info(ipcPoller.info) + logger.info(ipPoller.info) + // if we shutdown/close before the poller starts, we don't want to block forever + pollerClosedLatch = CountDownLatch(1) + networkEventPoller.submit( + action = object : EventActionOperator { + override fun invoke(): Int { + return if (!shutdownEventPoller) { // NOTE: regarding fragment limit size. Repeated calls to '.poll' will reassemble a fragment. // `.poll(handler, 4)` == `.poll(handler, 2)` + `.poll(handler, 2)` - // this checks to see if there are NEW clients on the handshake ports - pollCount += ipv4Poller.poll() - pollCount += ipv6Poller.poll() - - // this checks to see if there are NEW clients via IPC - pollCount += ipcPoller.poll() + // this checks to see if there are NEW clients to handshake with + var pollCount = ipcPoller.poll() + ipPoller.poll() // this manages existing clients (for cleanup + connection polling). This has a concurrent iterator, // so we can modify this as we go connections.forEach { connection -> - if (!connection.isClosedViaAeron()) { + if (connection.canPoll()) { // Otherwise, poll the connection for messages pollCount += connection.poll() } else { // If the connection has either been closed, or has expired, it needs to be cleaned-up/deleted. - logger.debug { "[${connection.id}/${connection.streamId}] connection expired" } + if (logger.isDebugEnabled) { + logger.debug("[${connection}] connection expired (cleanup)") + } + // the connection MUST be removed in the same thread that is processing events (it will be removed again in close, and that is expected) removeConnection(connection) - // this will call removeConnection again, but that is ok - // this is blocking, because the connection MUST be removed in the same thread that is processing events + // we already removed the connection, we can call it again without side effects connection.close() - - // have to manually notify the server-listenerManager that this connection was closed - // if the connection was MANUALLY closed (via calling connection.close()), then the connection-listenermanager is - // instantly notified and on cleanup, the server-listenermanager is called - - // this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback - actionDispatch.launch { - listenerManager.notifyDisconnect(connection) - } } } - // 0 means we idle. >0 means reset and don't idle (because there are likely more poll events) - pollIdleStrategy.idle(pollCount) + pollCount + } else { + // remove ourselves from processing + EventPoller.REMOVE } + } + }, + onClose = object : EventCloseOperator { + override fun invoke() { + val mustRestartDriverOnError = aeronDriver.internal.mustRestartDriverOnError + logger.debug("Server event dispatch closing...") - logger.debug { "Network event dispatch closing..." } + ipcPoller.close() + ipPoller.close() + + // clear all the handshake info + handshake.clear() - // we want to process **actual** close cleanup events on this thread as well, otherwise we will have threading problems - shutdownPollLatch.await() - // we have to manually cleanup the connections and call server-notifyDisconnect because otherwise this will never get called - val jobs = mutableListOf() + // we only need to run shutdown methods if there was a network outage or D/C + if (!shutdownInProgress.value) { + // this is because we restart automatically on driver errors + this@Server.close(closeEverything = false, sendDisconnectMessage = true, releaseWaitingThreads = !mustRestartDriverOnError) + } - // we want to clear all the connections FIRST (since we are shutting down) - val cons = mutableListOf() - connections.forEach { cons.add(it) } - connections.clear() - cons.forEach { connection -> - logger.info { "[${connection.id}/${connection.streamId}] Connection cleanup and close" } + if (mustRestartDriverOnError) { + logger.error("Critical driver error detected, restarting server.") - // make sure the connection is closed (close can only happen once, so a duplicate call does nothing!) - connection.close() + eventDispatch.CLOSE.launch { + waitForEndpointShutdown() - // have to manually notify the server-listenerManager that this connection was closed - // if the connection was MANUALLY closed (via calling connection.close()), then the connection-listenermanager is - // instantly notified and on cleanup, the server-listenermanager is called - // NOTE: this must be the LAST thing happening! + // also wait for everyone else to shutdown!! + aeronDriver.internal.endPointUsages.forEach { + if (it !== this@Server) { + it.waitForEndpointShutdown() + } + } - // this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback - val job = actionDispatch.launch { - listenerManager.notifyDisconnect(connection) - } - jobs.add(job) - } - // when we close a client or a server, we want to make sure that ALL notifications are finished. - // when it's just a connection getting closed, we don't care about this. We only care when it's "global" shutdown - runBlocking { - jobs.forEach { it.join() } - } - } catch (e: Exception) { - logger.error(e) { "Unexpected error during server message polling!" } - } finally { - ipv4Poller.close() - ipv6Poller.close() - ipcPoller.close() + // if we restart/reconnect too fast, errors from the previous run will still be present! + aeronDriver.delayLingerTimeout() - // clear all the handshake info - handshake.clear() + val p1 = this@Server.port1 + val p2 = this@Server.port2 - try { - // make sure that we have de-allocated all connection data - handshake.checkForMemoryLeaks() - } catch (e: AllocationException) { - logger.error(e) { "Error during server cleanup" } + if (p1 == 0 && p2 == 0) { + internalBind(port1 = 0, port2 = 0, onlyBindIpc = true, runShutdownCheck = false) + } else { + internalBind(port1 = p1, port2 = p2, onlyBindIpc = false, runShutdownCheck = false) + } + } } - // finish closing -- this lets us make sure that we don't run into race conditions on the thread that calls close() - try { - shutdownEventLatch.countDown() - } catch (ignored: Exception) {} + // we can now call bind again + endpointIsRunning.lazySet(false) + logger.debug("Closed the Network Event Poller task.") + pollerClosedLatch.countDown() } - } - config.networkInterfaceEventDispatcher.submit(networkEventProcessor) + }) + } + + +// /** +// * Adds an IP+subnet rule that defines what type of connection this IP+subnet should have. +// * - NOTHING : Nothing happens to the in/out bytes +// * - COMPRESS: The in/out bytes are compressed with LZ4-fast +// * - COMPRESS_AND_ENCRYPT: The in/out bytes are compressed (LZ4-fast) THEN encrypted (AES-256-GCM) +// * +// * If no rules are defined, then for LOOPBACK, it will always be `COMPRESS` and for everything else it will always be `COMPRESS_AND_ENCRYPT`. +// * +// * If rules are defined, then everything by default is `COMPRESS_AND_ENCRYPT`. +// * +// * The compression algorithm is LZ4-fast, so there is a small performance impact for a very large gain +// * Compress : 6.210 micros/op; 629.0 MB/s (output: 55.4%) +// * Uncompress : 0.641 micros/op; 6097.9 MB/s +// */ +// fun addConnectionRules(vararg rules: ConnectionRule) { +// connectionRules.addAll(listOf(*rules)) +// } - // wait for the polling thread to startup before letting bind() return - pollStartupLatch.await() + /** + * Adds an IP+subnet rule that defines if that IP+subnet is allowed/denied connectivity to this server. + * + * By default, if there are no filter rules, then all connections are allowed to connect + * If there are filter rules - then ONLY connections for the filter that returns true are allowed to connect (all else are denied) + * + * If ANY filter rule that is applied returns true, then the connection is permitted + * + * This function will be called for **only** network clients (IPC client are excluded) + * + * @param ipFilterRule the IpFilterRule to determine if this connection will be allowed to connect + */ + fun filter(ipFilterRule: IpFilterRule) { + listenerManager.filter(ipFilterRule) + } + + /** + * Adds a function that will be called BEFORE a client/server "connects" with each other, and used to determine if a connection + * should be allowed + * + * By default, if there are no filter rules, then all connections are allowed to connect + * If there are filter rules - then ONLY connections for the filter that returns true are allowed to connect (all else are denied) + * + * It is the responsibility of the custom filter to write the error, if there is one + * + * If the function returns TRUE, then the connection will continue to connect. + * If the function returns FALSE, then the other end of the connection will + * receive a connection error + * + * + * If ANY filter rule that is applied returns true, then the connection is permitted + * + * This function will be called for **only** network clients (IPC client are excluded) + * + * @param function clientAddress: UDP connection address + * tagName: the connection tag name + */ + fun filter(function: (clientAddress: InetAddress, tagName: String) -> Boolean) { + listenerManager.filter(function) } /** - * Adds an IP+subnet rule that defines what type of connection this IP+subnet should have. - * - NOTHING : Nothing happens to the in/out bytes - * - COMPRESS: The in/out bytes are compressed with LZ4-fast - * - COMPRESS_AND_ENCRYPT: The in/out bytes are compressed (LZ4-fast) THEN encrypted (AES-256-GCM) + * Adds a function that will be called BEFORE a client/server "connects" with each other, and used to determine if buffered messages + * for a connection should be enabled * - * If no rules are defined, then for LOOPBACK, it will always be `COMPRESS` and for everything else it will always be `COMPRESS_AND_ENCRYPT`. + * By default, if there are no rules, then all connections will have buffered messages enabled + * If there are rules - then ONLY connections for the rule that returns true will have buffered messages enabled (all else are disabled) * - * If rules are defined, then everything by default is `COMPRESS_AND_ENCRYPT`. + * It is the responsibility of the custom filter to write the error, if there is one * - * The compression algorithm is LZ4-fast, so there is a small performance impact for a very large gain - * Compress : 6.210 micros/op; 629.0 MB/s (output: 55.4%) - * Uncompress : 0.641 micros/op; 6097.9 MB/s + * If the function returns TRUE, then the buffered messages for a connection are enabled. + * If the function returns FALSE, then the buffered messages for a connection is disabled. + * + * If ANY rule that is applied returns true, then the buffered messages for a connection are enabled + * + * @param function clientAddress: not-null when UDP connection, null when IPC connection + * tagName: the connection tag name */ - fun addConnectionRules(vararg rules: ConnectionRule) { - connectionRules.addAll(listOf(*rules)) + fun enableBufferedMessages(function: (clientAddress: InetAddress?, tagName: String) -> Boolean) { + listenerManager.enableBufferedMessages(function) } /** @@ -411,20 +404,36 @@ open class Server( } /** - * Closes the server and all it's connections. After a close, you may call 'bind' again. + * Will throw an exception if there are resources that are still in use */ - final override fun close0() { - // when we call close, it will shutdown the polling mechanism, then wait for us to tell it to cleanup connections. - // - // Aeron + the Media Driver will have already been shutdown at this point. - if (bindAlreadyCalled.getAndSet(false)) { - // These are run in lock-step - shutdownPollLatch.countDown() - shutdownEventLatch.await() - } + fun checkForMemoryLeaks() { + AeronDriver.checkForMemoryLeaks() + + // make sure that we have de-allocated all connection data + handshake.checkForMemoryLeaks() } + /** + * By default, if you call close() on the server, it will shut down all parts of the endpoint (listeners, driver, event polling, etc). + * + * @param closeEverything if true, all parts of the server will be closed (listeners, driver, event polling, etc) + */ + fun close(closeEverything: Boolean = true) { + bufferedManager.close() + close(closeEverything = closeEverything, sendDisconnectMessage = true, releaseWaitingThreads = true) + } + override fun toString(): String { + return string0 + } + + fun use(block: (Server) -> R): R { + return try { + block(this) + } finally { + close() + } + } // /** // * Only called by the server! diff --git a/src/dorkbox/network/aeron/AeronContext.kt b/src/dorkbox/network/aeron/AeronContext.kt index 34a83abc..cac25bca 100644 --- a/src/dorkbox/network/aeron/AeronContext.kt +++ b/src/dorkbox/network/aeron/AeronContext.kt @@ -1,36 +1,43 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.aeron import dorkbox.network.Configuration -import dorkbox.util.NamedThreadFactory +import dorkbox.network.exceptions.AeronDriverException +import dorkbox.util.Sys import io.aeron.driver.MediaDriver import io.aeron.exceptions.DriverTimeoutException -import mu.KLogger +import org.slf4j.Logger +import java.io.Closeable import java.io.File -import java.util.concurrent.locks.* - -class AeronContext( - val config: Configuration, - val type: Class<*> = AeronDriver::class.java, - val logger: KLogger, - aeronErrorHandler: (error: Throwable) -> Unit -) { - fun close() { - context.close() - - // Destroys this thread group and all of its subgroups. - // This thread group must be empty, indicating that all threads that had been in this thread group have since stopped. - threadFactory.group.destroy() - } - +import java.util.concurrent.* + +/** + * Creates the Aeron Media Driver context + * + * @throws IllegalStateException if the configuration has already been used to create a context + * @throws IllegalArgumentException if the aeron media driver directory cannot be setup + */ +internal class AeronContext(config: Configuration.MediaDriverConfig, logger: Logger, aeronErrorHandler: (Throwable) -> Unit) : Closeable { companion object { - private fun create( - config: Configuration, - threadFactory: NamedThreadFactory, - aeronErrorHandler: (error: Throwable) -> Unit - - ): MediaDriver.Context { + private fun create(config: Configuration.MediaDriverConfig, aeronErrorHandler: (Throwable) -> Unit): MediaDriver.Context { // LOW-LATENCY SETTINGS - // .termBufferSparseFile(false) + // MediaDriver.Context() + // .termBufferSparseFile(false) // .useWindowsHighResTimer(true) // .threadingMode(ThreadingMode.DEDICATED) // .conductorIdleStrategy(BusySpinIdleStrategy.INSTANCE) @@ -42,53 +49,69 @@ class AeronContext( // setProperty("aeron.socket.so_rcvbuf", "2097152"); // setProperty("aeron.rcv.initial.window.length", "2097152"); + val threadFactory = Configuration.aeronThreadFactory + // driver context must happen in the initializer, because we have a Server.isRunning() method that uses the mediaDriverContext (without bind) val mediaDriverContext = MediaDriver.Context() + .termBufferSparseFile(false) // files occupy the same space virtually AND physically! + .useWindowsHighResTimer(true) + + // we assign our OWN ID! so we reserve everything. .publicationReservedSessionIdLow(AeronDriver.RESERVED_SESSION_ID_LOW) .publicationReservedSessionIdHigh(AeronDriver.RESERVED_SESSION_ID_HIGH) + .threadingMode(config.threadingMode) .mtuLength(config.networkMtuSize) + .ipcMtuLength(config.ipcMtuSize) .initialWindowLength(config.initialWindowLength) - .socketSndbufLength(config.sendBufferSize) - .socketRcvbufLength(config.receiveBufferSize) - mediaDriverContext .conductorThreadFactory(threadFactory) .receiverThreadFactory(threadFactory) .senderThreadFactory(threadFactory) .sharedNetworkThreadFactory(threadFactory) .sharedThreadFactory(threadFactory) - mediaDriverContext.aeronDirectoryName(config.aeronDirectory!!.absolutePath) - if (mediaDriverContext.ipcTermBufferLength() != io.aeron.driver.Configuration.ipcTermBufferLength()) { - // default 64 megs each is HUGE - mediaDriverContext.ipcTermBufferLength(8 * 1024 * 1024) + if (config.sendBufferSize > 0) { + mediaDriverContext.socketSndbufLength(config.sendBufferSize) } - if (mediaDriverContext.publicationTermBufferLength() != io.aeron.driver.Configuration.termBufferLength()) { - // default 16 megs each is HUGE (we run out of space in production w/ lots of clients) - mediaDriverContext.publicationTermBufferLength(2 * 1024 * 1024) + if (config.receiveBufferSize > 0) { + mediaDriverContext.socketRcvbufLength(config.receiveBufferSize) + } + + if (config.conductorIdleStrategy != null) { + mediaDriverContext.conductorIdleStrategy(config.conductorIdleStrategy) + } + if (config.sharedIdleStrategy != null) { + mediaDriverContext.sharedIdleStrategy(config.sharedIdleStrategy) + } + if (config.receiverIdleStrategy != null) { + mediaDriverContext.receiverIdleStrategy(config.receiverIdleStrategy) + } + if (config.senderIdleStrategy != null) { + mediaDriverContext.senderIdleStrategy(config.senderIdleStrategy) + } + + mediaDriverContext.aeronDirectoryName(config.aeronDirectory!!.path) + + if (config.ipcTermBufferLength > 0) { + mediaDriverContext.ipcTermBufferLength(config.ipcTermBufferLength) + } + + if (config.publicationTermBufferLength > 0) { + mediaDriverContext.publicationTermBufferLength(config.publicationTermBufferLength) } // we DO NOT want to abort the JVM if there are errors. // this replaces the default handler with one that doesn't abort the JVM - mediaDriverContext.errorHandler { error -> - aeronErrorHandler(error) - } + mediaDriverContext.errorHandler(aeronErrorHandler) return mediaDriverContext } } - // this is the aeron conductor/network processor thread factory which manages the incoming messages from the network. - internal val threadFactory = NamedThreadFactory( - "Aeron", - ThreadGroup("${type.simpleName}-AeronDriver"), Thread.MAX_PRIORITY, - true) - - // the context is validated before the AeronDriver object is created val context: MediaDriver.Context @@ -105,11 +128,17 @@ class AeronContext( * * @return the aeron context directory */ - val driverDirectory: File + val directory: File get() { return context.aeronDirectory() } + fun deleteAeronDir(): Boolean { + // NOTE: We must be *super* careful trying to delete directories, because if we have multiple AERON/MEDIA DRIVERS connected to the + // same directory, deleting the directory will cause any other aeron connection to fail! (which makes sense). + return directory.deleteRecursively() + } + /** * Checks to see if an endpoint (using the specified configuration) is running. * @@ -120,15 +149,20 @@ class AeronContext( return context.isDriverActive(context.driverTimeoutMs()) { } } + private fun isRunning(context: MediaDriver.Context): Boolean { + // if the media driver is running, it will be a quick connection. Usually 100ms or so + return try { + context.isDriverActive(context.driverTimeoutMs()) { } + } catch (e: Exception) { + false + } + } - /** - * Creates the Aeron Media Driver context - * - * @throws IllegalStateException if the configuration has already been used to create a context - * @throws IllegalArgumentException if the aeron media driver directory cannot be setup - */ - init { - var context = create(config, threadFactory, aeronErrorHandler) + init { + // NOTE: if a DIFFERENT PROCESS is using the SAME driver location, THERE WILL BE POTENTIAL PROBLEMS! + // ADDITIONALLY, the ONLY TIME we create a new aeron context is when it is the FIRST aeron context for a driver. Within the same + // JVM, the aeron driver/context is SHARED. + val context = create(config, aeronErrorHandler) // this happens EXACTLY once. Must be BEFORE the "isRunning" check! context.concludeAeronDirectory() @@ -139,57 +173,57 @@ class AeronContext( val driverTimeout = context.driverTimeoutMs() // sometimes when starting up, if a PREVIOUS run was corrupted (during startup, for example) - // we ONLY do this during the initial startup check because it will delete the directory, and we don't - // always want to do this. - var isRunning = try { + // we ONLY do this during the initial startup check because it will delete the directory, and we don't always want to do this. + + val isRunning = try { context.isDriverActive(driverTimeout) { } } catch (e: DriverTimeoutException) { // we have to delete the directory, since it was corrupted, and we try again. - if (aeronDir.deleteRecursively()) { + if (!config.forceAllowSharedAeronDriver && aeronDir.deleteRecursively()) { context.isDriverActive(driverTimeout) { } + } else if (config.forceAllowSharedAeronDriver) { + // we are expecting a shared directory. SOMETHING is screwed up! + throw AeronDriverException("Aeron was expected to be running, and the current location is corrupted. Not doing anything!", e) } else { // unable to delete the directory throw e } } - // this is incompatible with IPC, and will not be set if IPC is enabled - if (config.uniqueAeronDirectory && isRunning) { - val savedParent = aeronDir.parentFile - var retry = 0 - val retryMax = 100 - - while (config.uniqueAeronDirectory && isRunning) { - if (retry++ > retryMax) { - throw IllegalArgumentException("Unable to force unique aeron Directory. Tried $retryMax times and all tries were in use.") - } - - val randomNum = (1..retryMax).shuffled().first() - val newDir = savedParent.resolve("${aeronDir.name}_$randomNum") - - context = create(config, threadFactory, aeronErrorHandler) - context.aeronDirectoryName(newDir.path) - - // this happens EXACTLY once. Must be BEFORE the "isRunning" check! - context.concludeAeronDirectory() - - isRunning = context.isDriverActive(driverTimeout) { } + // only do this if we KNOW we are not running! + if (!isRunning) { + // NOTE: We must be *super* careful trying to delete directories, because if we have multiple AERON/MEDIA DRIVERS connected to the + // same directory, deleting the directory will cause any other aeron connection to fail! (which makes sense). + // make sure it's clean! + aeronDir.deleteRecursively() + + // if we are not CURRENTLY running, then we should ALSO delete it when we are done! + context.dirDeleteOnShutdown() + } else if (!config.forceAllowSharedAeronDriver) { + // maybe it's a mistake because we restarted too quickly! A brief pause to fix this! + + val timeoutInNs = TimeUnit.SECONDS.toNanos(config.connectionCloseTimeoutInSeconds.toLong()) + context.publicationLingerTimeoutNs() + val timeoutInMs = TimeUnit.NANOSECONDS.toMillis(timeoutInNs) + logger.warn("Aeron is currently running, waiting ${Sys.getTimePrettyFull(timeoutInNs)} for it to close.") + + // wait for it to close! wait longer. + val startTime = System.nanoTime() + while (isRunning(context) && System.nanoTime() - startTime < timeoutInNs) { + Thread.sleep(timeoutInMs) } - if (!isRunning) { - // NOTE: We must be *super* careful trying to delete directories, because if we have multiple AERON/MEDIA DRIVERS connected to the - // same directory, deleting the directory will cause any other aeron connection to fail! (which makes sense). - // since we are forcing a unique directory, we should ALSO delete it when we are done! - context.dirDeleteOnShutdown() - } + require(!isRunning(context)) { "Aeron is currently running, and this is the first instance created by this JVM. " + + "You must use `config.forceAllowSharedAeronDriver` to be able to re-use a shared aeron process at: $aeronDir" } } - logger.info { "Aeron directory: '${context.aeronDirectory()}'" } - this.context = context } override fun toString(): String { return context.toString() } + + override fun close() { + context.close() + } } diff --git a/src/dorkbox/network/aeron/AeronDriver.kt b/src/dorkbox/network/aeron/AeronDriver.kt index 08ec9f34..e46b80c1 100644 --- a/src/dorkbox/network/aeron/AeronDriver.kt +++ b/src/dorkbox/network/aeron/AeronDriver.kt @@ -1,37 +1,73 @@ +/* + * Copyright 2024 dorkbox, llc + * + * 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. + */ + +@file:Suppress("MemberVisibilityCanBePrivate") + package dorkbox.network.aeron +import dorkbox.collections.IntMap +import dorkbox.netUtil.IPv6 import dorkbox.network.Configuration +import dorkbox.network.connection.Connection +import dorkbox.network.connection.EndPoint import dorkbox.network.connection.ListenerManager -import dorkbox.network.exceptions.AeronDriverException -import dorkbox.network.exceptions.ClientRetryException -import io.aeron.Aeron -import io.aeron.ChannelUriStringBuilder -import io.aeron.CncFileDescriptor -import io.aeron.Publication -import io.aeron.Subscription -import io.aeron.driver.MediaDriver -import io.aeron.samples.SamplesUtil -import kotlinx.atomicfu.atomic -import mu.KLogger -import mu.KotlinLogging -import org.agrona.DirectBuffer -import org.agrona.SemanticVersion -import org.agrona.concurrent.BackoffIdleStrategy +import dorkbox.network.connection.ListenerManager.Companion.cleanAllStackTrace +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTraceInternal +import dorkbox.network.exceptions.AllocationException +import dorkbox.network.handshake.RandomId65kAllocator +import dorkbox.network.serialization.AeronOutput +import dorkbox.util.Sys +import io.aeron.* +import io.aeron.driver.reports.LossReportReader +import io.aeron.driver.reports.LossReportUtil +import io.aeron.logbuffer.BufferClaim +import io.aeron.protocol.DataHeaderFlyweight +import kotlinx.atomicfu.AtomicBoolean +import org.agrona.* +import org.agrona.concurrent.AtomicBuffer +import org.agrona.concurrent.IdleStrategy +import org.agrona.concurrent.UnsafeBuffer +import org.agrona.concurrent.errors.ErrorLogReader import org.agrona.concurrent.ringbuffer.RingBufferDescriptor import org.agrona.concurrent.status.CountersReader +import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.File -import java.lang.Thread.sleep +import java.io.IOException +import java.io.RandomAccessFile +import java.nio.MappedByteBuffer +import java.nio.channels.FileChannel +import java.util.concurrent.locks.* +import kotlin.concurrent.read +import kotlin.concurrent.write + +fun ChannelUriStringBuilder.endpoint(isIpv4: Boolean, addressString: String, port: Int): ChannelUriStringBuilder { + this.endpoint(AeronDriver.address(isIpv4, addressString, port)) + return this +} + fun ChannelUriStringBuilder.controlEndpoint(isIpv4: Boolean, addressString: String, port: Int): ChannelUriStringBuilder { + this.controlEndpoint(AeronDriver.address(isIpv4, addressString, port)) + return this +} /** * Class for managing the Aeron+Media drivers */ -class AeronDriver( - val config: Configuration, - val type: Class<*> = AeronDriver::class.java, - val logger: KLogger = KotlinLogging.logger("AeronConfig"), - aeronErrorLogger: (exception: Throwable) -> Unit = { error -> LoggerFactory.getLogger("AeronDriver").error("Aeron error", error) } -) { +class AeronDriver(config: Configuration, val logger: Logger, val endPoint: EndPoint<*>?) { companion object { /** @@ -49,96 +85,149 @@ class AeronDriver( */ internal const val RESERVED_SESSION_ID_HIGH = Integer.MAX_VALUE - const val UDP_HANDSHAKE_STREAM_ID: Int = 0x1337cafe - const val IPC_HANDSHAKE_STREAM_ID_PUB: Int = 0x1337c0de - const val IPC_HANDSHAKE_STREAM_ID_SUB: Int = 0x1337c0d3 - + // guarantee that session/stream ID's will ALWAYS be unique! (there can NEVER be a collision!) + val sessionIdAllocator = RandomId65kAllocator(RESERVED_SESSION_ID_LOW, RESERVED_SESSION_ID_HIGH) + val streamIdAllocator = RandomId65kAllocator((Short.MAX_VALUE * 2) - 1) // this is 65k-1 values - // on close, the publication CAN linger (in case a client goes away, and then comes back) - // AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) - private const val AERON_PUBLICATION_LINGER_TIMEOUT = 5_000L // in MS // prevents multiple instances, within the same JVM, from starting at the exact same time. - // since this is "global" and cannot be run in parallel, we DO NOT use coroutines! - private val lock = arrayOf(0) - private val mediaDriverUsageCount = atomic(0) - private val aeronClientUsageCount = atomic(0) - - private fun setConfigDefaults(config: Configuration, logger: KLogger) { - // explicitly don't set defaults if we already have the context defined! - if (config.contextDefined) { - return + private val lock = ReentrantReadWriteLock() + + // have to keep track of configurations and drivers, as we do not want to start the same media driver configuration multiple times (this causes problems!) + internal val driverConfigurations = IntMap(4) + + fun new(endPoint: EndPoint<*>): AeronDriver { + var driver: AeronDriver? + lock.write { + driver = AeronDriver(endPoint.config, endPoint.logger, endPoint) } - /* - * Linux - * Linux normally requires some settings of sysctl values. One is net.core.rmem_max to allow larger SO_RCVBUF and - * net.core.wmem_max to allow larger SO_SNDBUF values to be set. - * - * Windows - * Windows tends to use SO_SNDBUF values that are too small. It is recommended to use values more like 1MB or so. - * - * Mac/Darwin - * - * Mac tends to use SO_SNDBUF values that are too small. It is recommended to use larger values, like 16KB. - */ - if (config.receiveBufferSize == 0) { - config.receiveBufferSize = io.aeron.driver.Configuration.SOCKET_RCVBUF_LENGTH_DEFAULT - // when { - // OS.isLinux() -> - // OS.isWindows() -> - // OS.isMacOsX() -> - // } + return driver!! + } - // val rmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.rmem_max") - // val wmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.wmem_max") + + fun withLock(action: () -> Unit) { + lock.write { + action() } + } + /** + * Ensures that an endpoint (using the specified configuration) is NO LONGER running. + * + * @return true if the media driver is STOPPED. + */ + fun ensureStopped(configuration: Configuration, logger: Logger, timeout: Long): Boolean { + if (!isLoaded(configuration.copy(), logger)) { + return true + } - if (config.sendBufferSize == 0) { - config.sendBufferSize = io.aeron.driver.Configuration.SOCKET_SNDBUF_LENGTH_DEFAULT - // when { - // OS.isLinux() -> - // OS.isWindows() -> - // OS.isMacOsX() -> - // } + var stopped = false + lock.write { + stopped = AeronDriver(configuration, logger, null).use { + it.ensureStopped(timeout, 500) + } + } - // val rmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.rmem_max") - // val wmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.wmem_max") + // hacky, but necessary for multiple checks + configuration.contextDefined = false + return stopped + } + + /** + * Checks to see if a driver (using the specified configuration) is currently loaded. This specifically does NOT check if the + * driver is active/running!! + * + * @return true if the media driver is loaded. + */ + fun isLoaded(configuration: Configuration, logger: Logger): Boolean { + // not EVERYTHING is used for the media driver. For ** REUSING ** the media driver, only care about those specific settings + val mediaDriverConfig = getDriverConfig(configuration, logger) + + // assign the driver for this configuration. THIS IS GLOBAL for a JVM, because for a specific configuration, aeron only needs to be initialized ONCE. + // we have INSTANCE of the "wrapper" AeronDriver, because we want to be able to have references to the logger when doing things, + // however - the code that actually does stuff is a "singleton" in regard to an aeron configuration + return lock.read { + driverConfigurations[mediaDriverConfig.mediaDriverId()] != null } + } + /** + * Checks to see if a driver (using the specified configuration) is running. + * + * @return true if the media driver is active and running + */ + fun isRunning(configuration: Configuration, logger: Logger): Boolean { + var running = false + lock.read { + running = AeronDriver(configuration, logger, null).use { + it.isRunning() + } + } - /* - * Note: Since Mac OS does not have a built-in support for /dev/shm it is advised to create a RAM disk for the Aeron directory (aeron.dir). - * - * You can create a RAM disk with the following command: - * - * $ diskutil erasevolume HFS+ "DISK_NAME" `hdiutil attach -nomount ram://$((2048 * SIZE_IN_MB))` - * - * where: - * - * DISK_NAME should be replaced with a name of your choice. - * SIZE_IN_MB is the size in megabytes for the disk (e.g. 4096 for a 4GB disk). - * - * For example, the following command creates a RAM disk named DevShm which is 2GB in size: - * - * $ diskutil erasevolume HFS+ "DevShm" `hdiutil attach -nomount ram://$((2048 * 2048))` - * - * After this command is executed the new disk will be mounted under /Volumes/DevShm. - */ - if (config.aeronDirectory == null) { - val baseFileLocation = config.suggestAeronLogLocation(logger) + return running + } + + /** + * @return true if all JVM tracked Aeron drivers are closed, false otherwise + */ + fun areAllInstancesClosed(logger: Logger = LoggerFactory.getLogger(AeronDriver::class.java.simpleName)): Boolean { + return lock.read { + val traceEnabled = logger.isTraceEnabled - // val aeronLogDirectory = File(baseFileLocation, "aeron-" + type.simpleName) - val aeronLogDirectory = File(baseFileLocation, "aeron") - config.aeronDirectory = aeronLogDirectory + driverConfigurations.forEach { entry -> + val driver = entry.value + val closed = if (traceEnabled) driver.isInUse(null, logger) else driver.isRunning() + + if (closed) { + logger.error( "Aeron Driver [${driver.driverId}]: still running during check (${driver.aeronDirectory})") + return@read false + } + } + + if (!traceEnabled) { + // this is already checked if we are in trace mode. + driverConfigurations.forEach { entry -> + val driver = entry.value + if (driver.isInUse(null, logger)) { + logger.error("Aeron Driver [${driver.driverId}]: still in use during check (${driver.aeronDirectory})") + return@read false + } + } + } + + true + } + } + + /** + * @return the error code text for the specified number + */ + internal fun errorCodeName(result: Long): String { + return when (result) { + // The publication is not connected to a subscriber, this can be an intermittent state as subscribers come and go. + Publication.NOT_CONNECTED -> "Not connected" + + // The offer failed due to back pressure from the subscribers preventing further transmission. + Publication.BACK_PRESSURED -> "Back pressured" + + // The action is an operation such as log rotation which is likely to have succeeded by the next retry attempt. + Publication.ADMIN_ACTION -> "Administrative action" + + // The Publication has been closed and should no longer be used. + Publication.CLOSED -> "Publication is closed" + + // If this happens then the publication should be closed and a new one added. To make it less likely to happen then increase the term buffer length. + Publication.MAX_POSITION_EXCEEDED -> "Maximum term position exceeded" + + else -> throw IllegalStateException("Unknown error code: $result") } } private fun aeronCounters(aeronLocation: File): CountersReader? { val resolve = aeronLocation.resolve("cnc.dat") return if (resolve.exists()) { - val cncByteBuffer = SamplesUtil.mapExistingFileReadOnly(resolve) + val cncByteBuffer = mapExistingFileReadOnly(resolve) val cncMetaDataBuffer: DirectBuffer = CncFileDescriptor.createMetaDataBuffer(cncByteBuffer) CountersReader( @@ -174,14 +263,58 @@ class AeronDriver( } } + /** + * exposes the Aeron driver loss statistics + * + * @return the number of errors for the Aeron driver + */ + fun driverErrors(aeronLocation: File, errorAction: (observationCount: Int, firstObservationTimestamp: Long, lastObservationTimestamp: Long, encodedException: String) -> Unit): Int { + val errorMmap = mapExistingFileReadOnly(aeronLocation.resolve("cnc.dat")) + + try { + val buffer: AtomicBuffer = CommonContext.errorLogBuffer(errorMmap) + + return ErrorLogReader.read(buffer) { + observationCount: Int, firstObservationTimestamp: Long, lastObservationTimestamp: Long, encodedException: String -> + + errorAction(observationCount, firstObservationTimestamp, lastObservationTimestamp, encodedException) + } + } finally { + IoUtil.unmap(errorMmap) + } + } + + /** + * exposes the Aeron driver loss statistics + * + * @return the number of loss statistics for the Aeron driver + */ + fun driverLossStats(aeronLocation: File, lossStats: (observationCount: Long, + totalBytesLost: Long, + firstObservationTimestamp: Long, + lastObservationTimestamp: Long, + sessionId: Int, streamId: Int, + channel: String, source: String) -> Unit): Int { + + val lossReportFile = aeronLocation.resolve(LossReportUtil.LOSS_REPORT_FILE_NAME) + return if (lossReportFile.exists()) { + val mappedByteBuffer = mapExistingFileReadOnly(lossReportFile) + val buffer: AtomicBuffer = UnsafeBuffer(mappedByteBuffer) + + LossReportReader.read(buffer, lossStats) + } else { + 0 + } + } + /** * @return the internal heartbeat of the Aeron driver in the specified aeron directory */ fun driverHeartbeatMs(aeronLocation: File): Long { - val cncByteBuffer = SamplesUtil.mapExistingFileReadOnly(aeronLocation.resolve("cnc.dat")) + val cncByteBuffer = mapExistingFileReadOnly(aeronLocation.resolve("cnc.dat")) val cncMetaDataBuffer: DirectBuffer = CncFileDescriptor.createMetaDataBuffer(cncByteBuffer) - val toDriverBuffer = CncFileDescriptor.createToDriverBuffer(cncByteBuffer, cncMetaDataBuffer); + val toDriverBuffer = CncFileDescriptor.createToDriverBuffer(cncByteBuffer, cncMetaDataBuffer) val timestampOffset = toDriverBuffer.capacity() - RingBufferDescriptor.TRAILER_LENGTH + RingBufferDescriptor.CONSUMER_HEARTBEAT_OFFSET return toDriverBuffer.getLongVolatile(timestampOffset) @@ -191,7 +324,7 @@ class AeronDriver( * @return the internal version of the Aeron driver in the specified aeron directory */ fun driverVersion(aeronLocation: File): String { - val cncByteBuffer = SamplesUtil.mapExistingFileReadOnly(aeronLocation.resolve("cnc.dat")) + val cncByteBuffer = mapExistingFileReadOnly(aeronLocation.resolve("cnc.dat")) val cncMetaDataBuffer: DirectBuffer = CncFileDescriptor.createMetaDataBuffer(cncByteBuffer) val cncVersion = cncMetaDataBuffer.getInt(CncFileDescriptor.cncVersionOffset(0)) @@ -199,383 +332,752 @@ class AeronDriver( return cncSemanticVersion } - } - + /** + * Validates that all the resources have been freed (for all connections) + * + * note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD + */ + fun checkForMemoryLeaks() { + val sessionCounts = sessionIdAllocator.counts() + val streamCounts = streamIdAllocator.counts() + + if (sessionCounts > 0 || streamCounts > 0) { + throw AllocationException("Unequal allocate/free method calls for session/stream allocation: \n" + + "\tsession counts: $sessionCounts \n" + + "\tstream counts: $streamCounts" + ) + } + } - @Volatile - private var aeron: Aeron? = null - private var mediaDriver: MediaDriver? = null + fun uri(type: String, sessionId: Int, isReliable: Boolean): ChannelUriStringBuilder { + val builder = ChannelUriStringBuilder().media(type) + builder.reliable(isReliable) + // if a subscription has a session ID, then a publication MUST MATCH for it to connect (even if it has the correct stream ID/port) + builder.sessionId(sessionId) - // did WE start the media driver, or did SOMEONE ELSE start it? - private var mediaDriverWasAlreadyRunning = false + return builder + } + /** + * Do not use a session ID when we are a handshake connection! + */ + fun uriHandshake(type: String, isReliable: Boolean): ChannelUriStringBuilder { + val builder = ChannelUriStringBuilder().media(type) + builder.reliable(isReliable) - /** - * This is the error handler for Aeron *SPECIFIC* error messages. - */ - private val aeronErrorHandler: (error: Throwable) -> Unit + return builder + } - @Volatile - private var context_: AeronContext? = null - private val context: AeronContext - get() { - if (context_ == null) { - context_ = AeronContext(config, type, logger, aeronErrorHandler) + fun address(isIpv4: Boolean, addressString: String, port: Int): String { + return if (isIpv4) { + "$addressString:$port" + } else if (addressString[0] == '[') { + // IPv6 requires the address to be bracketed by [...] + "$addressString:$port" + } else { + // there MUST be [] surrounding the IPv6 address for aeron to like it! + "[$addressString]:$port" } - - return context_!! } + /** + * This will return the local-address of the interface that connects with the remote address (instead of on ALL interfaces) + */ + fun getLocalAddressString(publication: Publication, isRemoteIpv4: Boolean): String { + val localSocketAddress = publication.localSocketAddresses() + if (localSocketAddress == null || localSocketAddress.isEmpty()) { + throw Exception("The local socket address for the publication ${publication.channel()} is null/empty.") + } - init { - config.validate() // this happens more than once! (this is ok) - setConfigDefaults(config, logger) - - // cannot make any more changes to the configuration! - config.contextDefined = true + val localAddresses = localSocketAddress.first() + val splitPoint = localAddresses.lastIndexOf(':') + var localAddressString = localAddresses.substring(0, splitPoint) - val aeronErrorFilter = config.aeronErrorFilter - aeronErrorHandler = { error -> - if (aeronErrorFilter(error)) { - ListenerManager.cleanStackTrace(error) - aeronErrorLogger(AeronDriverException(error)) + return if (isRemoteIpv4) { + localAddressString + } else { + // this is necessary to clean up the address when adding it to aeron, since different formats mess it up + // aeron IPv6 addresses all have [...] + localAddressString = localAddressString.substring(1, localAddressString.length-1) + IPv6.toString(IPv6.toAddress(localAddressString)!!) } } - } - /** - * If the driver is not already running, this will start the driver. This will ALSO connect to the aeron client - * - * @return true if we are successfully connected to the aeron client - */ - fun start(): Boolean { - synchronized(lock) { - val mediaDriverLoaded = mediaDriverWasAlreadyRunning || mediaDriver != null - val isLoaded = mediaDriverLoaded && aeron != null && aeron?.isClosed == false - if (isLoaded) { - return true + /** + * This will return the local-address of the interface that connects with the remote address (instead of on ALL interfaces) + */ + fun getLocalAddressString(subscription: Subscription): String { + val localSocketAddress = subscription.localSocketAddresses() + if (localSocketAddress == null || localSocketAddress.isEmpty()) { + throw Exception("The local socket address for the subscription ${subscription.channel()} is null/empty.") } - if (!mediaDriverWasAlreadyRunning && mediaDriver == null) { - // only start if we didn't already start... There will be several checks. + val addressesAndPorts = localSocketAddress.first() + val splitPoint2 = addressesAndPorts.lastIndexOf(':') + return addressesAndPorts.substring(0, splitPoint2) + } - var running = isRunning() - if (running) { - // wait for a bit, because we are running, but we ALSO issued a START, and expect it to start. - // SOMETIMES aeron is in the middle of shutting down, and this prevents us from trying to connect to - // that instance - logger.debug { "Aeron Media driver already running. Double checking status..." } - sleep(context.driverTimeout/2) - running = isRunning() - } - if (!running) { - logger.debug { "Starting Aeron Media driver." } - - // try to start. If we start/stop too quickly, it's a problem - var count = 10 - while (count-- > 0) { - try { - mediaDriver = MediaDriver.launch(context.context) - logger.debug { "Started the Aeron Media driver." } - mediaDriverUsageCount.getAndIncrement() - break - } catch (e: Exception) { - logger.warn(e) { "Unable to start the Aeron Media driver at ${context.driverDirectory}. Retrying $count more times..." } - sleep(context.driverTimeout) - } - } - } else { - mediaDriverWasAlreadyRunning = true - logger.debug { "Not starting Aeron Media driver. It was already running." } - } - // if we were unable to load the aeron driver, don't continue. - if (!running && mediaDriver == null) { - logger.error { "Not running and unable to start the Aeron Media driver at ${context.driverDirectory}." } - return false - } - } - } + internal fun getDriverConfig(config: Configuration, logger: Logger): Configuration.MediaDriverConfig { + val mediaDriverConfig = Configuration.MediaDriverConfig(config) - // the media driver MIGHT already be started in a different process! - // - // We still ALWAYS want to connect to aeron (which connects to the other media driver process), especially if we - // haven't already connected to it (or if there was an error connecting because a different media driver was shutting down) + // this happens more than once! (this is ok) + config.validate() - val aeronDriverContext = Aeron.Context() - aeronDriverContext - .aeronDirectoryName(context.driverDirectory.path) - .concludeAeronDirectory() + mediaDriverConfig.validate() - aeronDriverContext - .threadFactory(context.threadFactory) - .idleStrategy(BackoffIdleStrategy()) + require(!config.contextDefined) { "Aeron configuration [${config.mediaDriverId()}] has already been initialized, unable to reuse this configuration!" } - // we DO NOT want to abort the JVM if there are errors. - // this replaces the default handler with one that doesn't abort the JVM - aeronDriverContext.errorHandler { error -> - aeronErrorHandler(error) + // cannot make any more changes to the configuration! + config.initialize(logger) + + // technically possible, but practically unlikely because of the different values calculated + require(mediaDriverConfig.mediaDriverId() != 0) { "There has been a severe error when calculating the media configuration ID. Aborting" } + + return mediaDriverConfig } - aeronDriverContext.subscriberErrorHandler { error -> - aeronErrorHandler(error) + + /** + * Map an existing file as a read only buffer. + * + * @param location of file to map. + * @return the mapped file. + */ + fun mapExistingFileReadOnly(location: File): MappedByteBuffer? { + if (!location.exists()) { + val msg = "file not found: " + location.absolutePath + throw IllegalStateException(msg) + } + var mappedByteBuffer: MappedByteBuffer? = null + try { + RandomAccessFile(location, "r").use { file -> + file.channel.use { channel -> + mappedByteBuffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()) + } + } + } catch (ex: IOException) { + LangUtil.rethrowUnchecked(ex) + } + return mappedByteBuffer } + } + private val logEverything = endPoint != null + internal val internal: AeronDriverInternal - // this might succeed if we can connect to the media driver - aeron = Aeron.connect(aeronDriverContext) - logger.debug { "Connected to Aeron driver." } - aeronClientUsageCount.getAndIncrement() + init { + // not EVERYTHING is used for the media driver. For ** REUSING ** the media driver, only care about those specific settings + val mediaDriverConfig = getDriverConfig(config, logger) - return true - } + // assign the driver for this configuration. THIS IS GLOBAL for a JVM, because for a specific configuration, aeron only needs to be initialized ONCE. + // we have INSTANCE of the "wrapper" AeronDriver, because we want to be able to have references to the logger when doing things, + // however - the code that actually does stuff is a "singleton" in regard to an aeron configuration + val driverId = mediaDriverConfig.mediaDriverId() - fun addPublication(publicationUri: ChannelUriStringBuilder, streamId: Int): Publication { - val uri = publicationUri.build() + logger.debug("Aeron Driver [$driverId]: Initializing...") + val aeronDriver = driverConfigurations.get(driverId) + if (aeronDriver == null) { + val driver = AeronDriverInternal(endPoint, mediaDriverConfig, logger) - // reasons we cannot add a pub/sub to aeron - // 1) the driver was closed - // 2) aeron was unable to connect to the driver - // 3) the address already in use + driverConfigurations.put(driverId, driver) - // configuring pub/sub to aeron is LINEAR -- and it happens in 2 places. - // 1) starting up the client/server - // 2) creating a new client-server connection pair (the media driver won't be "dead" at this point) + // register a logger so that we are notified when there is an error in Aeron + driver.addError { + logger.error("Aeron Driver [$driverId]: error!", this) + } - // in the client, if we are unable to connect to the server, we will attempt to start the media driver + connect to aeron + if (logEverything && logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Creating at '${driver.aeronDirectory}'") + } - val aeron1 = aeron - if (aeron1 == null || aeron1.isClosed) { - // there was an error connecting to the aeron client or media driver. - val ex = ClientRetryException("Error adding a publication to aeron") - ListenerManager.cleanAllStackTrace(ex) - throw ex - } + internal = driver + } else { + if (logEverything && logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Reusing driver") + } - val publication = try { - aeron1.addPublication(uri, streamId) - } catch (e: Exception) { - // this happens if the aeron media driver cannot actually establish connection - val ex = ClientRetryException("Error adding a publication", e) - ListenerManager.cleanAllStackTrace(ex) - throw ex - } + // assign our endpoint to the driver + aeronDriver.addEndpoint(endPoint) - if (publication == null) { - // there was an error connecting to the aeron client or media driver. - val ex = ClientRetryException("Error adding a publication") - ListenerManager.cleanAllStackTrace(ex) - throw ex + internal = aeronDriver } + } + - return publication + /** + * This does TWO things + * - start the media driver if not already running + * - connect the aeron client to the running media driver + * + * @return true if we are successfully connected to the aeron client + */ + fun start() = lock.write { + internal.start(logger) } - fun addSubscription(subscriptionUri: ChannelUriStringBuilder, streamId: Int): Subscription { - val uri = subscriptionUri.build() + /** + * For publications, if we add them "too quickly" (faster than the 'linger' timeout), Aeron will throw exceptions. + * ESPECIALLY if it is with the same streamID + * + * The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs. + */ + fun waitForConnection( + shutdown: AtomicBoolean, + publication: Publication, + handshakeTimeoutNs: Long, + logInfo: String, + onErrorHandler: (Throwable) -> Exception + ) { + if (publication.isConnected) { + return + } - // reasons we cannot add a pub/sub to aeron - // 1) the driver was closed - // 2) aeron was unable to connect to the driver - // 3) the address already in use + val startTime = System.nanoTime() - // configuring pub/sub to aeron is LINEAR -- and it happens in 2 places. - // 1) starting up the client/server - // 2) creating a new client-server connection pair (the media driver won't be "dead" at this point) + while (System.nanoTime() - startTime < handshakeTimeoutNs) { + if (publication.isConnected) { + return + } + if (shutdown.value) { + break + } - // in the client, if we are unable to connect to the server, we will attempt to start the media driver + connect to aeron + Thread.sleep(200L) + } + var closeException: Exception? = null + try { + // we might not be able to close this connection. + close(publication, logInfo) + } + catch (e: Exception) { + closeException = e + } - // subscriptions do not depend on a response from the remote endpoint, and should always succeed if aeron is available + val exception = onErrorHandler(Exception("Aeron Driver [${internal.driverId}]: Publication timed out in ${Sys.getTimePrettyFull(handshakeTimeoutNs)} while waiting for connection state: ${publication.channel()} streamId=${publication.streamId()}", closeException)) + exception.cleanAllStackTrace() + throw exception + } - val aeron1 = aeron - if (aeron1 == null || aeron1.isClosed) { - // there was an error connecting to the aeron client or media driver. - val ex = ClientRetryException("Error adding a subscription to aeron") - ListenerManager.cleanAllStackTrace(ex) - throw ex + /** + * For subscriptions, in the client we want to guarantee that the remote server has connected BACK to us! + */ + fun waitForConnection( + shutdown: AtomicBoolean, + subscription: Subscription, + handshakeTimeoutNs: Long, + logInfo: String, + onErrorHandler: (Throwable) -> Exception + ) { + if (subscription.isConnected) { + return } - val subscription = try { - aeron1.addSubscription(uri, streamId) - } catch (e: Exception) { - val ex = ClientRetryException("Error adding a subscription", e) - ListenerManager.cleanAllStackTrace(ex) - throw ex + val startTime = System.nanoTime() + + while (System.nanoTime() - startTime < handshakeTimeoutNs) { + if (subscription.isConnected && subscription.imageCount() > 0) { + return + } + if (shutdown.value) { + break + } + + Thread.sleep(200L) } - if (subscription == null) { - // there was an error connecting to the aeron client or media driver. - val ex = ClientRetryException("Error adding a subscription") - ListenerManager.cleanAllStackTrace(ex) - throw ex + var closeException: Exception? = null + try { + // we might not be able to close this connection. + close(subscription, logInfo) + } + catch (e: Exception) { + closeException = e } - return subscription + + val exception = onErrorHandler(Exception("Aeron Driver [${internal.driverId}]: Subscription timed out in ${Sys.getTimePrettyFull(handshakeTimeoutNs)} while waiting for connection state: ${subscription.channel()} streamId=${subscription.streamId()}", closeException)) + exception.cleanAllStackTrace() + throw exception } /** - * Checks to see if an endpoint (using the specified configuration) is running. + * Add a [ConcurrentPublication] for publishing messages to subscribers. * - * @return true if the media driver is active and running + * This guarantees that the publication is added and ACTIVE + * + * The publication returned is thread-safe. */ - fun isRunning(): Boolean { - // if the media driver is running, it will be a quick connection. Usually 100ms or so - return context.isRunning() + fun addPublication(publicationUri: ChannelUriStringBuilder, streamId: Int, logInfo: String, isIpc: Boolean): Publication { + return internal.addPublication(logger, publicationUri, streamId, logInfo, isIpc) } /** - * A safer way to try to close the media driver if in the ENTIRE JVM, our process is the only one using aeron with it's specific configuration + * Add an [ExclusivePublication] for publishing messages to subscribers from a single thread. * - * NOTE: We must be *super* careful trying to delete directories, because if we have multiple AERON/MEDIA DRIVERS connected to the - * same directory, deleting the directory will cause any other aeron connection to fail! (which makes sense). + * This guarantees that the publication is added and ACTIVE + * + * This is not a thread-safe publication! */ - fun closeIfSingle() { - if (!mediaDriverWasAlreadyRunning && aeronClientUsageCount.value == 1 && mediaDriverUsageCount.value == 1) { - close() - } + fun addExclusivePublication(publicationUri: ChannelUriStringBuilder, streamId: Int, logInfo: String, isIpc: Boolean): Publication { + return internal.addExclusivePublication(logger, publicationUri, streamId, logInfo, isIpc) } + /** + * Add a new [Subscription] for subscribing to messages from publishers. + * + * This guarantees that the subscription is added and ACTIVE + * + * The method will set up the [Subscription] to use the + * {@link Aeron.Context#availableImageHandler(AvailableImageHandler)} and + * {@link Aeron.Context#unavailableImageHandler(UnavailableImageHandler)} from the {@link Aeron.Context}. + */ + fun addSubscription(subscriptionUri: ChannelUriStringBuilder, streamId: Int, logInfo: String, isIpc: Boolean): Subscription { + return internal.addSubscription(logger, subscriptionUri, streamId, logInfo, isIpc) + } /** - * A safer way to try to close the media driver + * Guarantee that the publication is closed AND the backing file is removed. * - * NOTE: We must be *super* careful trying to delete directories, because if we have multiple AERON/MEDIA DRIVERS connected to the - * same directory, deleting the directory will cause any other aeron connection to fail! (which makes sense). + * On close, the publication CAN linger (in case a client goes away, and then comes back) + * AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) + * + * This can throw exceptions! */ - fun close() { - synchronized(lock) { - try { - aeron?.close() - aeronClientUsageCount.getAndDecrement() - } catch (e: Exception) { - logger.error(e) { "Error stopping aeron." } - } + fun close(publication: Publication, logInfo: String) { + internal.close(publication, logger, logInfo) + } - aeron = null + /** + * Guarantee that the publication is closed AND the backing file is removed + * + * This can throw exceptions! + */ + fun close(subscription: Subscription, logInfo: String) { + internal.close(subscription, logger, logInfo) + } - if (mediaDriver == null) { - logger.debug { "No driver started for this instance. Not Stopping." } - return - } - if (mediaDriverWasAlreadyRunning) { - logger.debug { "We did not start the media driver, so we are not stopping it." } - return - } + /** + * Ensures that an endpoint (using the specified configuration) is NO LONGER running. + * + * @return true if the media driver is STOPPED. + */ + fun ensureStopped(timeoutMS: Long, intervalTimeoutMS: Long): Boolean = + internal.ensureStopped(timeoutMS, intervalTimeoutMS, logger) + /** + * Checks to see if an endpoint (using the specified configuration) is running. + * + * @return true if the media driver is active and running + */ + fun isRunning(): Boolean = internal.isRunning() - logger.debug { "Stopping driver at '${context.driverDirectory}'..." } + /** + * Deletes the entire context of the aeron directory in use. + */ + fun deleteAeronDir() = internal.deleteAeronDir() - if (!isRunning()) { - // not running - logger.debug { "Driver is not running at '${context.driverDirectory}' for this context. Not Stopping." } - return - } + /** + * Checks to see if an endpoint (using the specified configuration) was previously closed. + * + * @return true if the media driver was explicitly closed + */ + fun closed() = internal.closed() - // if we are the ones that started the media driver, then we must be the ones to close it - try { - mediaDriverUsageCount.getAndDecrement() - mediaDriver!!.close() - } catch (e: Exception) { - logger.error(e) { "Error closing the Aeron media driver" } - } + fun isInUse(endPoint: EndPoint<*>?): Boolean = internal.isInUse(endPoint, logger) - mediaDriver = null + /** + * @return the aeron media driver log file for a specific publication. + */ + fun getMediaDriverFile(publication: Publication): File { + return internal.getMediaDriverFile(publication) + } - // it can actually close faster, if everything is ideal. - if (isRunning()) { - // on close, we want to wait for the driver to timeout before considering it "closed". Connections can still LINGER (see below) - // on close, the publication CAN linger (in case a client goes away, and then comes back) - // AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) - sleep(context.driverTimeout + AERON_PUBLICATION_LINGER_TIMEOUT) - } + /** + * @return the aeron media driver log file for a specific image (within a subscription, an image is the "connection" with a publication). + */ + fun getMediaDriverFile(image: Image): File { + return internal.getMediaDriverFile(image) + } - // wait for the media driver to actually stop - var count = 10 - while (--count >= 0 && isRunning()) { - logger.warn { "Aeron Media driver at '${context.driverDirectory}' is still running. Waiting for it to stop. Trying to close $count more times." } - sleep(context.driverTimeout) - } - logger.debug { "Closed the media driver at '${context.driverDirectory}'" } + /** + * Deletes the logfile for this publication + */ + fun deleteLogFile(publication: Publication) { + internal.deleteLogFile(publication) + } - try { - } catch (e: Exception) { - logger.error(e) {"Error closing the media driver at '${context.driverDirectory}'" } - } + /** + * Deletes the logfile for this image (within a subscription, an image is the "connection" with a publication). + */ + fun deleteLogFile(image: Image) { + internal.deleteLogFile(image) + } - // make sure the context is also closed. - context.close() - try { - val deletedAeron = context.driverDirectory.deleteRecursively() - if (!deletedAeron) { - logger.error { "Error deleting aeron directory ${context.driverDirectory} on shutdown "} - } - } catch (e: Exception) { - logger.error(e) { "Error deleting Aeron directory at: ${context.driverDirectory}"} - } + /** + * expose the internal counters of the Aeron driver + */ + fun driverCounters(counterFunction: (counterId: Int, counterValue: Long, typeId: Int, keyBuffer: DirectBuffer?, label: String?) -> Unit) = + internal.driverCounters(counterFunction) - context_ = null - } - } + /** + * @return the backlog statistics for the Aeron driver + */ + fun driverBacklog(): BacklogStat? = internal.driverBacklog() /** - * @return the aeron driver timeout + * @return the internal heartbeat of the Aeron driver in the current aeron directory */ - fun driverTimeout(): Long { - return context.driverTimeout - } + fun driverHeartbeatMs(): Long = internal.driverHeartbeatMs() /** - * @return the aeron media driver log file for a specific publication. This should be removed when a publication is closed (but is not always!) + * exposes the Aeron driver loss statistics + * + * @return the number of errors for the Aeron driver */ - fun getMediaDriverPublicationFile(publicationRegId: Long): File { - return context.driverDirectory.resolve("publications").resolve("${publicationRegId}.logbuffer") - } + fun driverErrors(errorAction: (observationCount: Int, firstObservationTimestamp: Long, lastObservationTimestamp: Long, encodedException: String) -> Unit) = + internal.driverErrors(errorAction) /** - * @return the internal counters of the Aeron driver in the current aeron directory + * exposes the Aeron driver loss statistics + * + * @return the number of loss statistics for the Aeron driver */ - fun driverCounters(counterFunction: (counterId: Int, counterValue: Long, typeId: Int, keyBuffer: DirectBuffer?, label: String?) -> Unit) { - driverCounters(context.driverDirectory, counterFunction) - } + fun driverLossStats(lossStats: (observationCount: Long, + totalBytesLost: Long, + firstObservationTimestamp: Long, + lastObservationTimestamp: Long, + sessionId: Int, streamId: Int, + channel: String, source: String) -> Unit): Int = + internal.driverLossStats(lossStats) /** - * @return the backlog statistics for the Aeron driver + * @return the internal version of the Aeron driver in the current aeron directory + */ + fun driverVersion(): String = internal.driverVersion() + + /** + * @return the current aeron context info, if any */ - fun driverBacklog(): BacklogStat? { - return driverBacklog(context.driverDirectory) + fun contextInfo(): String = internal.contextInfo() + + /** + * @return Time in nanoseconds a publication will linger once it is drained to recover potential tail loss. + */ + fun lingerNs(): Long = internal.lingerNs() + + /** + * @return Time in nanoseconds a publication will be considered not connected if no status messages are received. + */ + fun publicationConnectionTimeoutNs(): Long { + return internal.publicationConnectionTimeoutNs() } /** - * @return the internal heartbeat of the Aeron driver in the current aeron directory + * Make sure that we DO NOT approach the Aeron linger timeout! + */ + fun delayLingerTimeout(multiplier: Number = 1) = internal.delayLingerTimeout(multiplier.toDouble()) + + /** + * A safer way to try to close the media driver if in the ENTIRE JVM, our process is the only one using aeron with it's specific configuration + * + * NOTE: We must be *super* careful trying to delete directories, because if we have multiple AERON/MEDIA DRIVERS connected to the + * same directory, deleting the directory will cause any other aeron connection to fail! (which makes sense). + * + * @return true if the driver was successfully stopped. */ - fun driverHeartbeatMs(): Long { - return driverHeartbeatMs(context.driverDirectory) + fun closeIfSingle(): Boolean = lock.write { + if (!isInUse(endPoint)) { + if (logEverything) { + internal.close(endPoint, logger) + } else { + internal.close(endPoint, Configuration.NOP_LOGGER) + } + } else { + false + } + } + + override fun toString(): String { + return internal.toString() } /** - * @return the internal version of the Aeron driver in the current aeron directory + * A safer way to try to close the media driver + * + * NOTE: We must be *super* careful trying to delete directories, because if we have multiple AERON/MEDIA DRIVERS connected to the + * same directory, deleting the directory will cause any other aeron connection to fail! (which makes sense). + * + * @return true if the driver was successfully stopped. */ - fun driverVersion(): String { - return driverVersion(context.driverDirectory) + fun close(): Boolean = lock.write { + if (logEverything) { + internal.close(endPoint, logger) + } else { + internal.close(endPoint, Configuration.NOP_LOGGER) + } + } + + fun use(block: (AeronDriver) -> R): R { + return try { + block(this) + } finally { + close() + } } + /** - * @return the current aeron context info, if any + * NOTE: This cannot be on a coroutine, because our kryo instances are NOT threadsafe! + * + * the actual bits that send data on the network. + * + * There is a maximum length allowed for messages which is the min of 1/8th a term length or 16MB. + * Messages larger than this should chunked using an application level chunking protocol. Chunking has better recovery + * properties from failure and streams with mechanical sympathy. + * + * This can be overridden if you want to customize exactly how data is sent on the network + * + * @param publication the connection specific publication + * @param internalBuffer the internal buffer that will be copied to the Aeron network driver + * @param offset the offset in the internal buffer at which to start copying bytes + * @param objectSize the number of bytes to copy (starting at the offset) + * @param connection the connection object + * + * @return true if the message was successfully sent by aeron, false otherwise. Exceptions are caught and NOT rethrown! */ - fun contextInfo(): String { - return context.toString() + internal fun send( + publication: Publication, + internalBuffer: MutableDirectBuffer, + bufferClaim: BufferClaim, + offset: Int, + objectSize: Int, + sendIdleStrategy: IdleStrategy, + connection: Connection, + abortEarly: Boolean, + listenerManager: ListenerManager + ): Boolean { + var result: Long + while (true) { + // The maximum claimable length is given by the maxPayloadLength() function, which is the MTU length less header (with defaults this is 1,376 bytes). + result = publication.tryClaim(objectSize, bufferClaim) + if (result >= 0) { + // success! + try { + // both .offer and .putBytes add bytes to the underlying termBuffer -- HOWEVER, putBytes is faster as there are no + // extra checks performed BECAUSE we have to do our own data fragmentation management. + // It doesn't make sense to use `.offer`, which ALSO has its own fragmentation handling (which is extra overhead for us) + bufferClaim.buffer().putBytes(DataHeaderFlyweight.HEADER_LENGTH, internalBuffer, offset, objectSize) + return true + } catch (e: Exception) { + logger.error("Error adding data to aeron buffer.", e) + return false + } finally { + // must commit() or abort() before the unblock timeout (default 15 seconds) occurs. + bufferClaim.commit() + } + } + + if (internal.mustRestartDriverOnError) { + logger.error("Critical error, not able to send data.") + // there were critical errors. Don't even try anything! we will reconnect automatically (on the client) when it shuts-down (the connection is closed immediately when an error of this type is encountered + + // aeron will likely report this is as "BACK PRESSURE" + return false + } + + /** + * Since the publication is not connected, we weren't able to send data to the remote endpoint. + */ + val endPoint = endPoint!! + if (result == Publication.NOT_CONNECTED) { + if (abortEarly) { + val exception = endPoint.newException( + "[${publication.sessionId()}] Unable to send message. (Connection in non-connected state, aborted attempt! ${errorCodeName(result)})" + ) + listenerManager.notifyError(exception) + return false + } + else if (publication.isConnected) { + // more critical error sending the message. we shouldn't retry or anything. + val errorMessage = "[${publication.sessionId()}] Error sending message. (Connection in non-connected state longer than linger timeout. ${errorCodeName(result)})" + + // either client or server. No other choices. We create an exception, because it's more useful! + val exception = endPoint.newException(errorMessage) + + // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is + // where we see who is calling "send()" + exception.cleanStackTrace(3) + listenerManager.notifyError(exception) + return false + } + else { + // by default, we BUFFER data on a connection -- so the message will be placed into a queue to be re-sent once the connection comes back + // no extra actions required by us. + // Returning a "false" here makes sure that the session manager picks-up this message to e-broadcast (eventually) on the updated connection + return false + } + } + + /** + * The publication is not connected to a subscriber, this can be an intermittent state as subscribers come and go. + * val NOT_CONNECTED: Long = -1 + * + * The offer failed due to back pressure from the subscribers preventing further transmission. + * val BACK_PRESSURED: Long = -2 + * + * The offer failed due to an administration action and should be retried. + * The action is an operation such as log rotation which is likely to have succeeded by the next retry attempt. + * val ADMIN_ACTION: Long = -3 + */ + if (result >= Publication.ADMIN_ACTION) { + // we should retry, BUT we want to block ANYONE ELSE trying to write at the same time! + sendIdleStrategy.idle() + continue + } + + + if (result == Publication.CLOSED && connection.isClosed()) { + // this can happen when we use RMI to close a connection. RMI will (in most cases) ALWAYS send a response when it's + // done executing. If the connection is *closed* first (because an RMI method closed it), then we will not be able to + // send the message. + return false + } + + // more critical error sending the message. we shouldn't retry or anything. + val errorMessage = "[${publication.sessionId()}] Error sending message. (${errorCodeName(result)})" + + // either client or server. No other choices. We create an exception, because it's more useful! + val exception = endPoint.newException(errorMessage) + + // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is + // where we see who is calling "send()" + exception.cleanStackTrace(3) + listenerManager.notifyError(exception) + return false + } } /** - * @return the publication linger timeout. With IPC connections, another publication WITHIN the linger timeout will - * cause errors inside of Aeron + * NOTE: this **MUST** stay on the same co-routine that calls "send". This cannot be re-dispatched onto a different coroutine! + * CANNOT be called in action dispatch. ALWAYS ON SAME THREAD + * Server -> will be network polling thread + * Client -> will be thread that calls `connect()` + * + * @return true if the message was successfully sent by aeron */ - fun getLingerNs(): Long { - return context.context.publicationLingerTimeoutNs() + internal fun send( + publication: Publication, + buffer: AeronOutput, + logInfo: String, + listenerManager: ListenerManager, + handshakeSendIdleStrategy: IdleStrategy + ): Boolean { + val objectSize = buffer.position() + val internalBuffer = buffer.internalBuffer + + var result: Long + while (true) { + result = publication.offer(internalBuffer, 0, objectSize) + if (result >= 0) { + // success! + return true + } + + if (internal.mustRestartDriverOnError) { + // there were critical errors. Don't even try anything! we will reconnect automatically (on the client) when it shuts-down (the connection is closed immediately when an error of this type is encountered + + // aeron will likely report this is as "BACK PRESSURE" + return false + } + + /** + * Since the publication is not connected, we weren't able to send data to the remote endpoint. + * + * According to Aeron Docs, Pubs and Subs can "come and go", whatever that means. We just want to make sure that we + * don't "loop forever" if a publication is ACTUALLY closed, like on purpose. + */ + val endPoint = endPoint!! + if (result == Publication.NOT_CONNECTED) { + if (publication.isConnected) { + // more critical error sending the message. we shouldn't retry or anything. + // this exception will be a ClientException or a ServerException + val exception = endPoint.newException( + "[$logInfo] Error sending message. (Connection in non-connected state longer than linger timeout. ${errorCodeName(result)})", + null + ) + + exception.cleanStackTraceInternal() + listenerManager.notifyError(exception) + throw exception + } + else { + // publication was actually closed, so no bother throwing an error + return false + } + } + + /** + * The publication is not connected to a subscriber, this can be an intermittent state as subscribers come and go. + * val NOT_CONNECTED: Long = -1 + * + * The offer failed due to back pressure from the subscribers preventing further transmission. + * val BACK_PRESSURED: Long = -2 + * + * The offer failed due to an administration action and should be retried. + * The action is an operation such as log rotation which is likely to have succeeded by the next retry attempt. + * val ADMIN_ACTION: Long = -3 + */ + if (result >= Publication.ADMIN_ACTION) { + // we should retry. + handshakeSendIdleStrategy.idle() + continue + } + + if (result == Publication.CLOSED) { + // this can happen when we use RMI to close a connection. RMI will (in most cases) ALWAYS send a response when it's + // done executing. If the connection is *closed* first (because an RMI method closed it), then we will not be able to + // send the message. + return false + } + + // more critical error sending the message. we shouldn't retry or anything. + val errorMessage = "[${publication.sessionId()}] Error sending message. (${errorCodeName(result)})" + + // either client or server. No other choices. We create an exception, because it's more useful! + val exception = endPoint.newException(errorMessage) + + // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is + // where we see who is calling "send()" + exception.cleanStackTrace(3) + listenerManager.notifyError(exception) + return false + } + } + + fun newIfClosed(): AeronDriver { + endPoint!! + + var driver: AeronDriver? = null + + withLock { + driver = if (closed()) { + // Only starts the media driver if we are NOT already running! + try { + AeronDriver(endPoint.config, endPoint.logger, endPoint) + } catch (e: Exception) { + throw endPoint.newException("Error initializing aeron driver", e) + } + } else { + this + } + } + + return driver!! } } diff --git a/src/dorkbox/network/aeron/AeronDriverInternal.kt b/src/dorkbox/network/aeron/AeronDriverInternal.kt new file mode 100644 index 00000000..e22308aa --- /dev/null +++ b/src/dorkbox/network/aeron/AeronDriverInternal.kt @@ -0,0 +1,1168 @@ +/* + * Copyright 2024 dorkbox, llc + * + * 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 dorkbox.network.aeron + +import dorkbox.collections.ConcurrentIterator +import dorkbox.collections.LockFreeHashSet +import dorkbox.network.Configuration +import dorkbox.network.connection.EndPoint +import dorkbox.network.connection.ListenerManager +import dorkbox.network.connection.ListenerManager.Companion.cleanAllStackTrace +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTraceInternal +import dorkbox.network.exceptions.AeronDriverException +import dorkbox.network.exceptions.ClientRetryException +import io.aeron.* +import io.aeron.driver.MediaDriver +import io.aeron.status.ChannelEndpointStatus +import kotlinx.atomicfu.atomic +import org.agrona.DirectBuffer +import org.agrona.concurrent.BackoffIdleStrategy +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import java.io.IOException +import java.net.BindException +import java.net.SocketException +import java.util.concurrent.* +import java.util.concurrent.locks.* +import kotlin.concurrent.write + +internal class AeronDriverInternal(endPoint: EndPoint<*>?, config: Configuration.MediaDriverConfig, logger: Logger) { + companion object { + // on close, the publication CAN linger (in case a client goes away, and then comes back) + // AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) + private const val AERON_PUBLICATION_LINGER_TIMEOUT = 5_000L // in MS + + private const val AERON_PUB_SUB_TIMEOUT = 50L // in MS + + private val driverLogger = LoggerFactory.getLogger(AeronDriver::class.java.simpleName) + + private val onErrorGlobalList = atomic(Array Unit>(0) { { } }) + private val onErrorGlobalLock = ReentrantReadWriteLock() + + /** + * Called when there is an Aeron error + */ + fun onError(function: Throwable.() -> Unit) { + onErrorGlobalLock.write { + // we have to follow the single-writer principle! + onErrorGlobalList.lazySet(ListenerManager.add(function, onErrorGlobalList.value)) + } + } + + private fun removeOnError(function: Throwable.() -> Unit) { + onErrorGlobalLock.write { + // we have to follow the single-writer principle! + onErrorGlobalList.lazySet(ListenerManager.remove(function, onErrorGlobalList.value)) + } + } + + + /** + * Invoked when there is a global error (no connection information) + * + * The error is also sent to an error log before notifying callbacks + */ + fun notifyError(exception: Throwable) { + onErrorGlobalList.value.forEach { + try { + it(exception) + } catch (t: Throwable) { + // NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace + t.cleanStackTrace() + driverLogger.error("Global error with Aeron", t) + } + } + } + + init { + // fix the transport poller for java 17! + FixTransportPoller.init() + } + } + + val driverId = config.mediaDriverId() + + internal val endPointUsages = ConcurrentIterator>() + + @Volatile + private var aeron: Aeron? = null + private var mediaDriver: MediaDriver? = null + + private val onErrorLocalList = mutableListOf Unit>() + private val onErrorLocalLock = ReentrantReadWriteLock() + + private val context: AeronContext + private val aeronErrorHandler: (Throwable) -> Unit + + private val registeredPublications = atomic(0) + private val registeredSubscriptions = atomic(0) + + private val registeredPublicationsTrace: LockFreeHashSet = LockFreeHashSet() + private val registeredSubscriptionsTrace: LockFreeHashSet = LockFreeHashSet() + + private val stateLock = ReentrantReadWriteLock() + + /** + * Checks to see if there are any critical network errors (for example, a VPN connection getting disconnected while running) + */ + @Volatile + internal var mustRestartDriverOnError = false + + @Volatile + private var closedTime = 0L + + @Volatile + private var closed = false + + fun closed(): Boolean { + return closed + } + + val aeronDirectory: File + get() { + return context.context.aeronDirectory() + } + + init { + // configure the aeron error handler + val filter = config.aeronErrorFilter + aeronErrorHandler = { error -> + // NOTE: this is an error callback for MANY things, MOST of them are ASYNC! This means that a messages can successfully be ADDED + // to aeron, but NOT successfully sent over the network. + + // this is bad! We must close this connection. THIS WILL BE CALLED AS FAST AS THE CPU CAN RUN (because of how aeron works). + if (!mustRestartDriverOnError) { + var restartNetwork = false + + // if the network interface is removed (for example, a VPN connection). + if (error is io.aeron.exceptions.ChannelEndpointException || + error.cause is BindException || + error.cause is SocketException || + error.cause is IOException) { + + restartNetwork = true + + if (error.message?.startsWith("ERROR - channel error - Network is unreachable") == true) { + val exception = AeronDriverException("Aeron Driver [$driverId]: Network is disconnected or unreachable.") + exception.cleanAllStackTrace() + notifyError(exception) + } else if (error.message?.startsWith("WARN - failed to send") == true) { + val exception = AeronDriverException("Aeron Driver [$driverId]: Network socket error, can't send data.") + exception.cleanAllStackTrace() + notifyError(exception) + } + else if (error.message == "Can't assign requested address") { + val exception = AeronDriverException("Aeron Driver [$driverId]: Network socket error, can't assign requested address.") + exception.cleanAllStackTrace() + notifyError(exception) + } else { + error.cleanStackTrace() + // send this out to the listener-manager so we can be notified of global errors + notifyError(AeronDriverException("Aeron Driver [$driverId]: Unexpected error!", error.cause)) + } + } + else if (error is io.aeron.exceptions.AeronException) { + if (error.message?.startsWith("ERROR - unexpected close of heartbeat timestamp counter:") == true) { + restartNetwork = true + + val exception = AeronDriverException("Aeron Driver [$driverId]: HEARTBEAT error, can't continue.") + exception.cleanAllStackTrace() + notifyError(exception) + } + } + + + if (restartNetwork) { + notifyError(AeronDriverException("Critical network error internal to the Aeron Driver, restarting network!").cleanAllStackTrace()) + + // this must be set before anything else happens + mustRestartDriverOnError = true + + // close will make sure to run on a different thread + endPointUsages.forEach { + // we cannot send the DC message because the network layer has issues! + it.close(closeEverything = false, sendDisconnectMessage = false, releaseWaitingThreads = false) + } + } + } + + + // if we are restarting the network, ignore all future messages + if (!mustRestartDriverOnError && filter(error)) { + error.cleanStackTrace() + // send this out to the listener-manager so we can be notified of global errors + notifyError(AeronDriverException(error)) + } + } + + // @throws IllegalStateException if the configuration has already been used to create a context + // @throws IllegalArgumentException if the aeron media driver directory cannot be setup + context = AeronContext(config, logger, aeronErrorHandler) + + addEndpoint(endPoint) + } + + // always called within a mutex! + fun addEndpoint(endPoint: EndPoint<*>?) { + if (endPoint != null) { + if (!endPointUsages.contains(endPoint)) { + endPointUsages.add(endPoint) + } + } + } + + + fun addError(function: Throwable.() -> Unit) { + // always add this to the global one + onError(function) + + // this is so we can track all the added error listeners (and removed them when we close, since the DRIVER has a global list) + onErrorLocalLock.write { + onErrorLocalList.add(function) + } + } + + private fun removeErrors() { + onErrorLocalLock.write { + mustRestartDriverOnError = false + onErrorLocalList.forEach { + removeOnError(it) + } + } + } + + /** + * This does TWO things + * - start the media driver if not already running + * - connect the aeron client to the running media driver + * + * @return true if we are successfully connected to the aeron client + */ + fun start(logger: Logger): Boolean = stateLock.write { + require(!closed) { "Aeron Driver [$driverId]: Cannot start a driver that was closed. A new driver + context must be created" } + + val isLoaded = mediaDriver != null && aeron != null && aeron?.isClosed == false + if (isLoaded) { + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Already running... Not starting again.") + } + return true + } + + if (mediaDriver == null) { + // only start if we didn't already start... There will be several checks. + + var running = isRunning() + if (running) { + // wait for a bit, because we are running, but we ALSO issued a START, and expect it to start. + // SOMETIMES aeron is in the middle of shutting down, and this prevents us from trying to connect to + // that instance + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Already running. Double checking status...") + } + Thread.sleep(context.driverTimeout / 2) + running = isRunning() + } + + if (!running) { + // try to start. If we start/stop too quickly, it's a problem + var count = 10 + while (count-- > 0) { + try { + mediaDriver = MediaDriver.launch(context.context) + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Successfully started") + } + break + } catch (e: Exception) { + logger.warn("Aeron Driver [$driverId]: Unable to start at ${context.directory}. Retrying $count more times...", e) + Thread.sleep(context.driverTimeout) + } + } + } else if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Not starting. It was already running.") + } + + // if we were unable to load the aeron driver, don't continue. + if (!running && mediaDriver == null) { + logger.error("Aeron Driver [$driverId]: Not running and unable to start at ${context.directory}.") + return false + } + } + + // the media driver MIGHT already be started in a different process! + // + // We still ALWAYS want to connect to aeron (which connects to the other media driver process), especially if we + // haven't already connected to it (or if there was an error connecting because a different media driver was shutting down) + + val aeronDriverContext = Aeron.Context() + aeronDriverContext + .aeronDirectoryName(context.directory.path) + .concludeAeronDirectory() + + aeronDriverContext + .threadFactory(Configuration.aeronThreadFactory) + .idleStrategy(BackoffIdleStrategy()) + + // we DO NOT want to abort the JVM if there are errors. + // this replaces the default handler with one that doesn't abort the JVM + aeronDriverContext.errorHandler(aeronErrorHandler) + aeronDriverContext.subscriberErrorHandler(aeronErrorHandler) + + // this might succeed if we can connect to the media driver + aeron = Aeron.connect(aeronDriverContext) + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Connected to '${context.directory}'") + } + + return true + } + + + /** + * Add a [ConcurrentPublication] for publishing messages to subscribers. + * + * This guarantees that the publication is added and ACTIVE + * + * The publication returned is threadsafe. + */ + @Suppress("DEPRECATION") + fun addPublication( + logger: Logger, + publicationUri: ChannelUriStringBuilder, + streamId: Int, + logInfo: String, + isIpc: Boolean + ): Publication = stateLock.write { + + val uri = publicationUri.build() + + // reasons we cannot add a pub/sub to aeron + // 1) the driver was closed + // 2) aeron was unable to connect to the driver + // 3) the address already in use + // 4) the SESSION ID is already in use (note: a subscription with NO sessionID will let ANY publication sessionID connect to it) + + // configuring pub/sub to aeron is LINEAR -- and it happens in 2 places. + // 1) starting up the client/server + // 2) creating a new client-server connection pair (the media driver won't be "dead" at this point) + + // in the client, if we are unable to connect to the server, we will attempt to start the media driver + connect to aeron + + // adding a publication can fail + + val aeron1 = aeron + if (aeron1 == null || aeron1.isClosed) { + logger.error("Aeron Driver [$driverId]: Aeron is closed, error creating publication [$logInfo] :: sessionId=${publicationUri.sessionId()}, streamId=$streamId") + // there was an error connecting to the aeron client or media driver. + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding a publication to aeron") + ex.cleanAllStackTrace() + throw ex + } + + val publication: ConcurrentPublication? = try { + aeron1.addPublication(uri, streamId) + } catch (e: Exception) { + logger.error("Aeron Driver [$driverId]: Error creating publication [$logInfo] :: sessionId=${publicationUri.sessionId()}, streamId=$streamId", e) + + // this happens if the aeron media driver cannot actually establish connection... OR IF IT IS TOO FAST BETWEEN ADD AND REMOVE FOR THE SAME SESSION/STREAM ID! + e.cleanAllStackTrace() + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding a publication", e) + ex.cleanAllStackTrace() + throw ex + } + + if (publication == null) { + logger.error("Aeron Driver [$driverId]: Error creating publication (is null) [$logInfo] :: sessionId=${publicationUri.sessionId()}, streamId=$streamId") + + // there was an error connecting to the aeron client or media driver. + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding a publication") + ex.cleanAllStackTrace() + throw ex + } + + var hasDelay = false + while (publication.channelStatus() != ChannelEndpointStatus.ACTIVE || (!isIpc && publication.localSocketAddresses().isEmpty())) { + if (publication.channelStatus() == ChannelEndpointStatus.ERRORED) { + logger.error("Aeron Driver [$driverId]: Error creating publication (has errors) $logInfo :: sessionId=${publicationUri.sessionId()}, streamId=${streamId}") + + // there was an error connecting to the aeron client or media driver. + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding an publication") + ex.cleanAllStackTrace() + throw ex + } + + if (!hasDelay) { + hasDelay = true + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Delaying creation of publication [$logInfo] :: sessionId=${publicationUri.sessionId()}, streamId=${streamId}") + } + } + // the publication has not ACTUALLY been created yet! + Thread.sleep(AERON_PUB_SUB_TIMEOUT) + } + + if (hasDelay && logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Delayed creation of publication [$logInfo] :: sessionId=${publicationUri.sessionId()}, streamId=${streamId}") + } + + + registeredPublications.getAndIncrement() + if (logger.isTraceEnabled) { + registeredPublicationsTrace.add(publication.registrationId()) + } + + if (logger.isTraceEnabled) { + logger.trace("Aeron Driver [$driverId]: Creating publication [$logInfo] :: regId=${publication.registrationId()}, sessionId=${publication.sessionId()}, streamId=${publication.streamId()}, channel=${publication.channel()}") + } + return publication + } + + /** + * Add an [ExclusivePublication] for publishing messages to subscribers from a single thread. + * + * This guarantees that the publication is added and ACTIVE + * + * This is not a thread-safe publication! + */ + @Suppress("DEPRECATION") + fun addExclusivePublication( + logger: Logger, + publicationUri: ChannelUriStringBuilder, + streamId: Int, + logInfo: String, + isIpc: Boolean): Publication = stateLock.write { + + val uri = publicationUri.build() + + // reasons we cannot add a pub/sub to aeron + // 1) the driver was closed + // 2) aeron was unable to connect to the driver + // 3) the address already in use + // 4) the SESSION ID is already in use (note: a subscription with NO sessionID will let ANY publication sessionID connect to it) + + // configuring pub/sub to aeron is LINEAR -- and it happens in 2 places. + // 1) starting up the client/server + // 2) creating a new client-server connection pair (the media driver won't be "dead" at this point) + + // in the client, if we are unable to connect to the server, we will attempt to start the media driver + connect to aeron + + val aeron1 = aeron + if (aeron1 == null || aeron1.isClosed) { + logger.error("Aeron Driver [$driverId]: Aeron is closed, error creating ex-publication $logInfo :: sessionId=${publicationUri.sessionId()}, streamId=${streamId}") + + // there was an error connecting to the aeron client or media driver. + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding an ex-publication to aeron") + ex.cleanAllStackTrace() + throw ex + } + + // adding a publication can fail + + val publication: ExclusivePublication? = try { + aeron1.addExclusivePublication(uri, streamId) + } catch (e: Exception) { + logger.error("Aeron Driver [$driverId]: Error creating ex-publication $logInfo :: sessionId=${publicationUri.sessionId()}, streamId=${streamId}", e) + + // this happens if the aeron media driver cannot actually establish connection... OR IF IT IS TOO FAST BETWEEN ADD AND REMOVE FOR THE SAME SESSION/STREAM ID! + e.cleanAllStackTrace() + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding an ex-publication", e) + ex.cleanAllStackTrace() + throw ex + } + + if (publication == null) { + logger.error("Aeron Driver [$driverId]: Error creating ex-publication (is null) $logInfo :: sessionId=${publicationUri.sessionId()}, streamId=${streamId}") + + // there was an error connecting to the aeron client or media driver. + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding an ex-publication") + ex.cleanAllStackTrace() + throw ex + } + + var hasDelay = false + while (publication.channelStatus() != ChannelEndpointStatus.ACTIVE || (!isIpc && publication.localSocketAddresses().isEmpty())) { + if (publication.channelStatus() == ChannelEndpointStatus.ERRORED) { + logger.error("Aeron Driver [$driverId]: Error creating ex-publication (has errors) $logInfo :: sessionId=${publicationUri.sessionId()}, streamId=${streamId}") + + // there was an error connecting to the aeron client or media driver. + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding an ex-publication") + ex.cleanAllStackTrace() + throw ex + } + + + if (!hasDelay) { + hasDelay = true + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Delaying creation of ex-publication [$logInfo] :: sessionId=${publicationUri.sessionId()}, streamId=${streamId}") + } + } + // the publication has not ACTUALLY been created yet! + Thread.sleep(AERON_PUB_SUB_TIMEOUT) + } + + if (hasDelay) { + logger.debug("Aeron Driver [$driverId]: Delayed creation of publication [$logInfo] :: sessionId=${publicationUri.sessionId()}, streamId=${streamId}") + } + + registeredPublications.getAndIncrement() + if (logger.isTraceEnabled) { + registeredPublicationsTrace.add(publication.registrationId()) + } + + if (logger.isTraceEnabled) { + logger.trace("Aeron Driver [$driverId]: Creating ex-publication $logInfo :: regId=${publication.registrationId()}, sessionId=${publication.sessionId()}, streamId=${publication.streamId()}, channel=${publication.channel()}") + } + return publication + } + + /** + * Add a new [Subscription] for subscribing to messages from publishers. + * + * This guarantees that the subscription is added and ACTIVE + * + * The method will set up the [Subscription] to use the + * {@link Aeron.Context#availableImageHandler(AvailableImageHandler)} and + * {@link Aeron.Context#unavailableImageHandler(UnavailableImageHandler)} from the {@link Aeron.Context}. + */ + @Suppress("DEPRECATION") + fun addSubscription( + logger: Logger, + subscriptionUri: ChannelUriStringBuilder, + streamId: Int, + logInfo: String, + isIpc: Boolean): Subscription = stateLock.write { + + val uri = subscriptionUri.build() + + // reasons we cannot add a pub/sub to aeron + // 1) the driver was closed + // 2) aeron was unable to connect to the driver + // 3) the address already in use + // 4) the SESSION ID is already in use (note: a subscription with NO sessionID will let ANY publication sessionID connect to it) + + + // configuring pub/sub to aeron is LINEAR -- and it happens in 2 places. + // 1) starting up the client/server + // 2) creating a new client-server connection pair (the media driver won't be "dead" at this point) + + // in the client, if we are unable to connect to the server, we will attempt to start the media driver + connect to aeron + + + // subscriptions do not depend on a response from the remote endpoint, and should always succeed if aeron is available + + val aeron1 = aeron + if (aeron1 == null || aeron1.isClosed) { + logger.error("Aeron Driver [$driverId]: Aeron is closed, error creating subscription [$logInfo] :: sessionId=${subscriptionUri.sessionId()}, streamId=${streamId}") + + // there was an error connecting to the aeron client or media driver. + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding a subscription to aeron") + throw ex + } + + val subscription = try { + aeron1.addSubscription(uri, streamId) + } catch (e: Exception) { + logger.error("Aeron Driver [$driverId]: Error creating subscription [$logInfo] :: sessionId=${subscriptionUri.sessionId()}, streamId=${streamId}") + + e.cleanAllStackTrace() + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding a subscription", e) // maybe not retry? or not clientRetry? + ex.cleanStackTraceInternal() + throw ex + } + + if (subscription == null) { + logger.error("Aeron Driver [$driverId]: Error creating subscription (is null) [$logInfo] :: sessionId=${subscriptionUri.sessionId()}, streamId=${streamId}") + + // there was an error connecting to the aeron client or media driver. + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding a subscription") + ex.cleanStackTraceInternal() + throw ex + } + + var hasDelay = false + while (subscription.channelStatus() != ChannelEndpointStatus.ACTIVE || (!isIpc && subscription.localSocketAddresses().isEmpty())) { + if (subscription.channelStatus() == ChannelEndpointStatus.ERRORED) { + logger.error("Aeron Driver [$driverId]: Error creating subscription (has errors) $logInfo :: sessionId=${subscriptionUri.sessionId()}, streamId=${streamId}") + + // there was an error connecting to the aeron client or media driver. + val ex = ClientRetryException("Aeron Driver [$driverId]: Error adding an subscription") + ex.cleanAllStackTrace() + throw ex + } + + if (!hasDelay) { + hasDelay = true + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Delaying creation of subscription [$logInfo] :: sessionId=${subscriptionUri.sessionId()}, streamId=${streamId}") + } + } + // the subscription has not ACTUALLY been created yet! + Thread.sleep(AERON_PUB_SUB_TIMEOUT) + } + + if (hasDelay && logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Delayed creation of subscription [$logInfo] :: sessionId=${subscriptionUri.sessionId()}, streamId=${streamId}") + } + + registeredSubscriptions.getAndIncrement() + if (logger.isTraceEnabled) { + registeredSubscriptionsTrace.add(subscription.registrationId()) + } + + if (logger.isTraceEnabled) { + logger.trace("Aeron Driver [$driverId]: Creating subscription [$logInfo] :: regId=${subscription.registrationId()}, sessionId=${subscriptionUri.sessionId()}, streamId=${subscription.streamId()}, channel=${subscription.channel()}") + } + return subscription + } + + /** + * Guarantee that the publication is closed AND the backing file is removed + */ + fun close(publication: Publication, logger: Logger, logInfo: String) = stateLock.write { + val name = if (publication is ConcurrentPublication) { + "publication" + } else { + "ex-publication" + } + + val registrationId = publication.registrationId() + + if (logger.isTraceEnabled) { + logger.trace("Aeron Driver [$driverId]: Closing $name file [$logInfo] :: regId=$registrationId, sessionId=${publication.sessionId()}, streamId=${publication.streamId()}") + } + + + val aeron1 = aeron + if (aeron1 == null || aeron1.isClosed) { + val e = Exception("Aeron Driver [$driverId]: Error closing $name [$logInfo] :: sessionId=${publication.sessionId()}, streamId=${publication.streamId()}") + throw e + } + + try { + // This can throw exceptions! + publication.close() + } catch (e: Exception) { + logger.error("Aeron Driver [$driverId]: Unable to close [$logInfo] $name $publication", e) + } + + if (publication is ConcurrentPublication) { + // aeron is async. close() doesn't immediately close, it just submits the close command! + // THIS CAN TAKE A WHILE TO ACTUALLY CLOSE! + while (publication.isConnected || publication.channelStatus() == ChannelEndpointStatus.ACTIVE || aeron1.getPublication(registrationId) != null) { + Thread.sleep(AERON_PUB_SUB_TIMEOUT) + } + } else { + // aeron is async. close() doesn't immediately close, it just submits the close command! + // THIS CAN TAKE A WHILE TO ACTUALLY CLOSE! + while (publication.isConnected || publication.channelStatus() == ChannelEndpointStatus.ACTIVE || aeron1.getExclusivePublication(registrationId) != null) { + Thread.sleep(AERON_PUB_SUB_TIMEOUT) + } + } + + // deleting log files is generally not recommended in a production environment as it can result in data loss and potential disruption of the messaging system!! + + registeredPublications.decrementAndGet() + if (logger.isTraceEnabled) { + registeredPublicationsTrace.remove(publication.registrationId()) + } + } + + /** + * Guarantee that the publication is closed AND the backing file is removed + */ + fun close(subscription: Subscription, logger: Logger, logInfo: String) = stateLock.write { + if (logger.isTraceEnabled) { + logger.trace("Aeron Driver [$driverId]: Closing subscription [$logInfo] :: regId=${subscription.registrationId()}, sessionId=${subscription.images().firstOrNull()?.sessionId()}, streamId=${subscription.streamId()}") + } + + val aeron1 = aeron + if (aeron1 == null || aeron1.isClosed) { + val e = Exception("Aeron Driver [$driverId]: Error closing subscription [$logInfo] :: sessionId=${subscription.images().firstOrNull()?.sessionId()}, streamId=${subscription.streamId()}") + throw e + } + + try { + // This can throw exceptions! + subscription.close() + } catch (e: Exception) { + logger.error("Aeron Driver [$driverId]: Unable to close [$logInfo] subscription $subscription") + } + + // aeron is async. close() doesn't immediately close, it just submits the close command! + // THIS CAN TAKE A WHILE TO ACTUALLY CLOSE! + while (subscription.isConnected || subscription.channelStatus() == ChannelEndpointStatus.ACTIVE || subscription.images().isNotEmpty()) { + Thread.sleep(AERON_PUB_SUB_TIMEOUT) + if (logger.isTraceEnabled) { + logger.trace("Aeron Driver [$driverId]: Still closing sub!") + } + } + + // deleting log files is generally not recommended in a production environment as it can result in data loss and potential disruption of the messaging system!! + + registeredSubscriptions.decrementAndGet() + if (logger.isTraceEnabled) { + registeredSubscriptionsTrace.remove(subscription.registrationId()) + } + } + + /** + * Ensures that an endpoint (using the specified configuration) is NO LONGER running. + * + * @return true if the media driver is STOPPED. + */ + fun ensureStopped(timeoutMS: Long, intervalTimeoutMS: Long, logger: Logger): Boolean { + if (closed) { + return true + } + + val timeoutInNanos = TimeUnit.MILLISECONDS.toNanos(timeoutMS) + lingerNs() + var didLog = false + + val closeTimeoutTime = System.nanoTime() + while (isRunning() && System.nanoTime() - closeTimeoutTime < timeoutInNanos) { + // only emit the log info once. It's rather spammy otherwise! + if (!didLog) { + didLog = true + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Still running (${aeronDirectory}). Waiting for it to stop...") + } + } + Thread.sleep(intervalTimeoutMS) + } + + return !isRunning() + } + + /** + * Deletes the entire context of the aeron directory in use. + */ + fun deleteAeronDir(): Boolean { + return context.deleteAeronDir() + } + + /** + * Checks to see if an endpoint (using the specified configuration) is running. + * + * @return true if the media driver is active and running + */ + fun isRunning(): Boolean { + // if the media driver is running, it will be a quick connection. Usually 100ms or so + return context.isRunning() + } + + fun isInUse(endPoint: EndPoint<*>?, logger: Logger): Boolean { + // as many "sort-cuts" as we can for checking if the current Aeron Driver/client is still in use + if (!isRunning()) { + if (logger.isTraceEnabled) { + logger.trace("Aeron Driver [$driverId]: not running") + } + return false + } + + if (registeredPublications.value > 0) { + if (logger.isTraceEnabled) { + val elements = registeredPublicationsTrace.elements + val joined = elements.joinToString() + logger.trace("Aeron Driver [$driverId]: has publications: [$joined] (${registeredPublications.value} total)") + } else if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: has publications (${registeredPublications.value} total)") + } + return true + } + + if (registeredSubscriptions.value > 0) { + if (logger.isTraceEnabled) { + val elements = registeredSubscriptionsTrace.elements + val joined = elements.joinToString() + logger.trace("Aeron Driver [$driverId]: has subscriptions: [$joined] (${registeredSubscriptions.value} total)") + } else if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: has subscriptions (${registeredSubscriptions.value} total)") + } + return true + } + + if (endPointUsages.size() > 1 && !endPointUsages.contains(endPoint)) { + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: still referenced by ${endPointUsages.size()} endpoints") + } + return true + } + + // ignore the extra driver checks, because in SOME situations, when trying to reconnect upon an error, the + // driver gets into a bad state. When this happens, we cannot rely on the driver stat info! + if (mustRestartDriverOnError) { + return false + } + + // check to see if we ALREADY have loaded this location. + // null or empty snapshot means that this location is currently unused + // >0 can also happen because the location is old. It's not running, but still has info because it hasn't been cleaned up yet + // NOTE: This is only valid information if the media driver is running + var currentUsage = driverBacklog()?.snapshot()?.size ?: 0 + var count = 3 + + while (count > 0 && currentUsage > 0) { + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: usage is: $currentUsage, double checking status") + } + delayLingerTimeout() + currentUsage = driverBacklog()?.snapshot()?.size ?: 0 + count-- + + if (currentUsage == 0) { + return false + } + } + + + count = 3 + while (count > 0 && currentUsage > 0) { + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: usage is: $currentUsage, double checking status (long)") + } + delayDriverTimeout() + currentUsage = driverBacklog()?.snapshot()?.size ?: 0 + count-- + } + + if (currentUsage > 0 && logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: usage is: $currentUsage") + } + + return currentUsage > 0 + } + + /** + * A safer way to try to close the media driver + * + * NOTE: We must be *super* careful trying to delete directories, because if we have multiple AERON/MEDIA DRIVERS connected to the + * same directory, deleting the directory will cause any other aeron connection to fail! (which makes sense). + * + * @return true if the driver was successfully stopped. + */ + fun close(endPoint: EndPoint<*>?, logger: Logger): Boolean = stateLock.write { + if (endPoint != null) { + endPointUsages.remove(endPoint) + } + + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Requested close... (${endPointUsages.size()} endpoints still in use)") + } + + // ignore the extra driver checks, because in SOME situations, when trying to reconnect upon an error, the + if (isInUse(endPoint, logger)) { + if (mustRestartDriverOnError) { + // driver gets into a bad state. When this happens, we have to ignore "are we already in use" checks, BECAUSE the driver is now corrupted and unusable! + } + else { + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: in use, not shutting down this instance.") + } + + // reset our contextDefine value, so that this configuration can safely be reused + endPoint?.config?.contextDefined = false + return@write false + } + } + + val removed = AeronDriver.driverConfigurations[driverId] + if (removed == null) { + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: already closed. Ignoring close request.") + } + // reset our contextDefine value, so that this configuration can safely be reused + endPoint?.config?.contextDefined = false + return@write false + } + + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Closing...") + } + + // we have to assign context BEFORE we close, because the `getter` for context will create it if necessary + val aeronContext = context + val driverDirectory = aeronContext.directory + + try { + aeron?.close() + } catch (e: Exception) { + if (endPoint != null) { + endPoint.listenerManager.notifyError(AeronDriverException("Aeron Driver [$driverId]: Error stopping", e)) + } else { + logger.error("Aeron Driver [$driverId]: Error stopping", e) + } + } + + aeron = null + + + if (mediaDriver == null) { + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: No driver started, not stopping driver or context.") + } + } else { + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Stopping driver at '${driverDirectory}'...") + } + + // if we are the ones that started the media driver, then we must be the ones to close it + try { + mediaDriver!!.close() + } catch (e: Exception) { + if (endPoint != null) { + endPoint.listenerManager.notifyError(AeronDriverException("Aeron Driver [$driverId]: Error closing", e)) + } else { + logger.error("Aeron Driver [$driverId]: Error closing", e) + } + } + + mediaDriver = null + } + + + // it can actually close faster, if everything is ideal. + val timeout = (aeronContext.driverTimeout + AERON_PUBLICATION_LINGER_TIMEOUT) / 4 + + // it can actually close faster, if everything is ideal. + try { + if (isRunning()) { + // on close, we want to wait for the driver to timeout before considering it "closed". Connections can still LINGER (see below) + // on close, the publication CAN linger (in case a client goes away, and then comes back) + // AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) + Thread.sleep(timeout) + } + + // wait for the media driver to actually stop + var count = 10 + while (--count >= 0 && isRunning()) { + logger.warn("Aeron Driver [$driverId]: still running at '${driverDirectory}'. Waiting for it to stop. Trying to close $count more times.") + Thread.sleep(timeout) + } + } + catch (e: Exception) { + if (!mustRestartDriverOnError) { + logger.error("Error while checking isRunning() state.", e) + } + } + + // make sure the context is also closed, but ONLY if we are the last one + aeronContext.close() + + // make sure all of our message listeners are removed + removeErrors() + + try { + val deletedAeron = driverDirectory.deleteRecursively() + if (!deletedAeron) { + if (endPoint != null) { + endPoint.listenerManager.notifyError(AeronDriverException("Aeron Driver [$driverId]: Error deleting Aeron directory at: $driverDirectory")) + } else { + logger.error("Aeron Driver [$driverId]: Error deleting Aeron directory at: $driverDirectory") + } + } + } catch (e: Exception) { + if (endPoint != null) { + endPoint.listenerManager.notifyError(AeronDriverException("Aeron Driver [$driverId]: Error deleting Aeron directory at: $driverDirectory", e)) + } else { + logger.error("Aeron Driver [$driverId]: Error deleting Aeron directory at: $driverDirectory", e) + } + } + + // check to make sure it's actually deleted + if (driverDirectory.isDirectory) { + if (endPoint != null) { + endPoint.listenerManager.notifyError(AeronDriverException("Aeron Driver [$driverId]: Error deleting Aeron directory at: $driverDirectory")) + } else { + logger.error("Aeron Driver [$driverId]: Error deleting Aeron directory at: $driverDirectory") + } + } + + + // reset our contextDefine value, so that this configuration can safely be reused + endPoint?.config?.contextDefined = false + + // actually remove it, since we've passed all the checks to guarantee it's closed... + AeronDriver.driverConfigurations.remove(driverId) + + if (logger.isDebugEnabled) { + logger.debug("Aeron Driver [$driverId]: Closed the media driver at '${driverDirectory}'") + } + closed = true + closedTime = System.nanoTime() + + return@write true + } + + /** + * @return the aeron driver timeout + */ + private fun driverTimeout(): Long { + return context.driverTimeout + } + + + /** + * @return the aeron media driver log file for a specific publication. + */ + fun getMediaDriverFile(publication: Publication): File { + return context.directory.resolve("publications").resolve("${publication.registrationId()}.logbuffer") + } + + /** + * @return the aeron media driver log file for a specific image (within a subscription, an image is the "connection" with a publication). + */ + fun getMediaDriverFile(image: Image): File { + return context.directory.resolve("images").resolve("${image.correlationId()}.logbuffer") + } + + /** + * Deletes the logfile for this publication + */ + fun deleteLogFile(publication: Publication) { + getMediaDriverFile(publication).delete() + } + + /** + * Deletes the logfile for this image (within a subscription, an image is the "connection" with a publication). + */ + fun deleteLogFile(image: Image) { + val file = getMediaDriverFile(image) + if (driverLogger.isDebugEnabled) { + driverLogger.debug("Deleting log file: $image") + } + file.delete() + } + + /** + * expose the internal counters of the Aeron driver + */ + fun driverCounters(counterFunction: (counterId: Int, counterValue: Long, typeId: Int, keyBuffer: DirectBuffer?, label: String?) -> Unit) { + AeronDriver.driverCounters(context.directory, counterFunction) + } + + /** + * @return the backlog statistics for the Aeron driver + */ + fun driverBacklog(): BacklogStat? { + return AeronDriver.driverBacklog(context.directory) + } + + /** + * @return the internal heartbeat of the Aeron driver in the current aeron directory + */ + fun driverHeartbeatMs(): Long { + return AeronDriver.driverHeartbeatMs(context.directory) + } + + + /** + * exposes the Aeron driver loss statistics + * + * @return the number of errors for the Aeron driver + */ + fun driverErrors(errorAction: (observationCount: Int, firstObservationTimestamp: Long, lastObservationTimestamp: Long, encodedException: String) -> Unit) { + AeronDriver.driverErrors(context.directory, errorAction) + } + + /** + * exposes the Aeron driver loss statistics + * + * @return the number of loss statistics for the Aeron driver + */ + fun driverLossStats(lossStats: (observationCount: Long, + totalBytesLost: Long, + firstObservationTimestamp: Long, + lastObservationTimestamp: Long, + sessionId: Int, streamId: Int, + channel: String, source: String) -> Unit): Int { + return AeronDriver.driverLossStats(context.directory, lossStats) + } + + /** + * @return the internal version of the Aeron driver in the current aeron directory + */ + fun driverVersion(): String { + return AeronDriver.driverVersion(context.directory) + } + + /** + * @return the current aeron context info, if any + */ + fun contextInfo(): String { + return context.toString() + } + + /** + * @return Time in nanoseconds a publication will linger once it is drained to recover potential tail loss. + */ + fun lingerNs(): Long { + return context.context.publicationLingerTimeoutNs() + } + + /** + * @return Time in nanoseconds a publication will be considered not connected if no status messages are received. + */ + fun publicationConnectionTimeoutNs(): Long { + return context.context.publicationConnectionTimeoutNs() + } + + /** + * Make sure that we DO NOT approach the Aeron linger timeout! + */ + fun delayDriverTimeout(multiplier: Number = 1) { + Thread.sleep((driverTimeout() * multiplier.toDouble()).toLong()) + } + + /** + * Make sure that we DO NOT approach the Aeron linger timeout! If we have already passed it, do nothing. + */ + fun delayLingerTimeout(multiplier: Number = 1) { + val lingerTimeoutNs = (lingerNs() * multiplier.toDouble()).toLong() + val driverTimeoutSec = driverTimeout().coerceAtLeast(TimeUnit.NANOSECONDS.toSeconds(lingerTimeoutNs)) + val driverTimeoutNs = TimeUnit.SECONDS.toNanos(driverTimeoutSec) + + val elapsedNs = System.nanoTime() - closedTime + if (elapsedNs >= driverTimeoutNs) { + // timeout already expired, do nothing. + return + } + + // not always the full duration, but the duration since the close event + val adjustedTimeoutSec = TimeUnit.NANOSECONDS.toSeconds(driverTimeoutNs - elapsedNs) + Thread.sleep(adjustedTimeoutSec) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is AeronDriverInternal) return false + if (!super.equals(other)) return false + + return driverId == other.driverId + } + + override fun hashCode(): Int { + return driverId + } + + override fun toString(): String { + return "Aeron Driver [${driverId}]" + } +} diff --git a/src/dorkbox/network/aeron/AeronPoller.kt b/src/dorkbox/network/aeron/AeronPoller.kt index 7cb4836b..efc1374a 100644 --- a/src/dorkbox/network/aeron/AeronPoller.kt +++ b/src/dorkbox/network/aeron/AeronPoller.kt @@ -1,8 +1,23 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.aeron internal interface AeronPoller { fun poll(): Int fun close() - val info: String } diff --git a/src/dorkbox/network/aeron/BacklogStat.kt b/src/dorkbox/network/aeron/BacklogStat.kt index b76f0089..91801fa7 100644 --- a/src/dorkbox/network/aeron/BacklogStat.kt +++ b/src/dorkbox/network/aeron/BacklogStat.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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. + */ + /* * Copyright 2014-2022 Real Logic Limited. * @@ -33,22 +49,19 @@ import java.util.* * [StreamStat] counters. * * - * Each stream managed by the [io.aeron.driver.MediaDriver] will be sampled and printed out on [System.out]. - */ -class BacklogStat -/** - * Construct by using a [CountersReader] which can be obtained from [Aeron.countersReader]. + * Each stream managed by the [io.aeron.driver.MediaDriver] will be sampled * * @param counters to read for tracking positions. */ - (private val counters: CountersReader) { +class BacklogStat(private val counters: CountersReader) { /** * Take a snapshot of all the backlog information and group by stream. * * @return a snapshot of all the backlog information and group by stream. */ fun snapshot(): Map { - val streams: MutableMap = HashMap() + val streams = mutableMapOf() + counters.forEach { counterId: Int, typeId: Int, keyBuffer: DirectBuffer, _: String? -> if (typeId >= PublisherLimit.PUBLISHER_LIMIT_TYPE_ID && typeId <= ReceiverPos.RECEIVER_POS_TYPE_ID || typeId == SenderLimit.SENDER_LIMIT_TYPE_ID || typeId == PerImageIndicator.PER_IMAGE_TYPE_ID || typeId == PublisherPos.PUBLISHER_POS_TYPE_ID) { val key = StreamCompositeKey( diff --git a/src/dorkbox/network/aeron/CoroutineBackoffIdleStrategy.kt b/src/dorkbox/network/aeron/CoroutineBackoffIdleStrategy.kt deleted file mode 100644 index 3f8875d2..00000000 --- a/src/dorkbox/network/aeron/CoroutineBackoffIdleStrategy.kt +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright 2014-2020 Real Logic Limited. - * - * 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. - */ -package dorkbox.network.aeron - -import kotlinx.coroutines.delay -import kotlinx.coroutines.yield -import org.agrona.concurrent.BackoffIdleStrategy -import org.agrona.hints.ThreadHints - -abstract class BackoffIdleStrategyPrePad { - var p000: Byte = 0 - var p001: Byte = 0 - var p002: Byte = 0 - var p003: Byte = 0 - var p004: Byte = 0 - var p005: Byte = 0 - var p006: Byte = 0 - var p007: Byte = 0 - var p008: Byte = 0 - var p009: Byte = 0 - var p010: Byte = 0 - var p011: Byte = 0 - var p012: Byte = 0 - var p013: Byte = 0 - var p014: Byte = 0 - var p015: Byte = 0 - var p016: Byte = 0 - var p017: Byte = 0 - var p018: Byte = 0 - var p019: Byte = 0 - var p020: Byte = 0 - var p021: Byte = 0 - var p022: Byte = 0 - var p023: Byte = 0 - var p024: Byte = 0 - var p025: Byte = 0 - var p026: Byte = 0 - var p027: Byte = 0 - var p028: Byte = 0 - var p029: Byte = 0 - var p030: Byte = 0 - var p031: Byte = 0 - var p032: Byte = 0 - var p033: Byte = 0 - var p034: Byte = 0 - var p035: Byte = 0 - var p036: Byte = 0 - var p037: Byte = 0 - var p038: Byte = 0 - var p039: Byte = 0 - var p040: Byte = 0 - var p041: Byte = 0 - var p042: Byte = 0 - var p043: Byte = 0 - var p044: Byte = 0 - var p045: Byte = 0 - var p046: Byte = 0 - var p047: Byte = 0 - var p048: Byte = 0 - var p049: Byte = 0 - var p050: Byte = 0 - var p051: Byte = 0 - var p052: Byte = 0 - var p053: Byte = 0 - var p054: Byte = 0 - var p055: Byte = 0 - var p056: Byte = 0 - var p057: Byte = 0 - var p058: Byte = 0 - var p059: Byte = 0 - var p060: Byte = 0 - var p061: Byte = 0 - var p062: Byte = 0 - var p063: Byte = 0 -} - -abstract class BackoffIdleStrategyData( - protected val maxSpins: Long, protected val maxYields: Long, protected val minParkPeriodMs: Long, protected val maxParkPeriodMs: Long) : BackoffIdleStrategyPrePad() { - - protected var state = 0 // NOT_IDLE - protected var spins: Long = 0 - protected var yields: Long = 0 - protected var parkPeriodMs: Long = 0 -} - -/** - * Idling strategy for threads when they have no work to do. - *

- * Spin for maxSpins, then - * [Coroutine.yield] for maxYields, then - * [Coroutine.delay] on an exponential backoff to maxParkPeriodMs - */ -@Suppress("unused") -class CoroutineBackoffIdleStrategy : BackoffIdleStrategyData, CoroutineIdleStrategy { - var p064: Byte = 0 - var p065: Byte = 0 - var p066: Byte = 0 - var p067: Byte = 0 - var p068: Byte = 0 - var p069: Byte = 0 - var p070: Byte = 0 - var p071: Byte = 0 - var p072: Byte = 0 - var p073: Byte = 0 - var p074: Byte = 0 - var p075: Byte = 0 - var p076: Byte = 0 - var p077: Byte = 0 - var p078: Byte = 0 - var p079: Byte = 0 - var p080: Byte = 0 - var p081: Byte = 0 - var p082: Byte = 0 - var p083: Byte = 0 - var p084: Byte = 0 - var p085: Byte = 0 - var p086: Byte = 0 - var p087: Byte = 0 - var p088: Byte = 0 - var p089: Byte = 0 - var p090: Byte = 0 - var p091: Byte = 0 - var p092: Byte = 0 - var p093: Byte = 0 - var p094: Byte = 0 - var p095: Byte = 0 - var p096: Byte = 0 - var p097: Byte = 0 - var p098: Byte = 0 - var p099: Byte = 0 - var p100: Byte = 0 - var p101: Byte = 0 - var p102: Byte = 0 - var p103: Byte = 0 - var p104: Byte = 0 - var p105: Byte = 0 - var p106: Byte = 0 - var p107: Byte = 0 - var p108: Byte = 0 - var p109: Byte = 0 - var p110: Byte = 0 - var p111: Byte = 0 - var p112: Byte = 0 - var p113: Byte = 0 - var p114: Byte = 0 - var p115: Byte = 0 - var p116: Byte = 0 - var p117: Byte = 0 - var p118: Byte = 0 - var p119: Byte = 0 - var p120: Byte = 0 - var p121: Byte = 0 - var p122: Byte = 0 - var p123: Byte = 0 - var p124: Byte = 0 - var p125: Byte = 0 - var p126: Byte = 0 - var p127: Byte = 0 - - companion object { - private const val NOT_IDLE = 0 - private const val SPINNING = 1 - private const val YIELDING = 2 - private const val PARKING = 3 - - /** - * Name to be returned from [.alias]. - */ - const val ALIAS = "backoff" - - /** - * Default number of times the strategy will spin without work before going to next state. - */ - const val DEFAULT_MAX_SPINS = 10L - - /** - * Default number of times the strategy will yield without work before going to next state. - */ - const val DEFAULT_MAX_YIELDS = 5L - - /** - * Default interval the strategy will park the thread on entering the park state in milliseconds. - */ - const val DEFAULT_MIN_PARK_PERIOD_MS = 1L - - /** - * Default interval the strategy will park the thread will expand interval to as a max in milliseconds. - */ - const val DEFAULT_MAX_PARK_PERIOD_MS = 1000L - } - - /** - * Default constructor using [.DEFAULT_MAX_SPINS], [.DEFAULT_MAX_YIELDS], [.DEFAULT_MIN_PARK_PERIOD_MS], and [.DEFAULT_MAX_PARK_PERIOD_MS]. - */ - constructor() : super(DEFAULT_MAX_SPINS, DEFAULT_MAX_YIELDS, DEFAULT_MIN_PARK_PERIOD_MS, DEFAULT_MAX_PARK_PERIOD_MS) {} - - /** - * Create a set of state tracking idle behavior - *

- * @param maxSpins to perform before moving to [Coroutine.yield] - * @param maxYields to perform before moving to [Coroutine.delay] - * @param minParkPeriodMs to use when initiating parking - * @param maxParkPeriodMs to use for end duration when parking - */ - constructor(maxSpins: Long, maxYields: Long, minParkPeriodMs: Long, maxParkPeriodMs: Long) - : super(maxSpins, maxYields, minParkPeriodMs, maxParkPeriodMs) { - } - - /** - * Perform current idle action (e.g. nothing/yield/sleep). This method signature expects users to call into it on - * every work 'cycle'. The implementations may use the indication "workCount > 0" to reset internal backoff - * state. This method works well with 'work' APIs which follow the following rules: - *

    - *
  • 'work' returns a value larger than 0 when some work has been done
  • - *
  • 'work' returns 0 when no work has been done
  • - *
  • 'work' may return error codes which are less than 0, but which amount to no work has been done
  • - *
- *

- * Callers are expected to follow this pattern: - * - *

-     * 
-     * while (isRunning)
-     * {
-     *     idleStrategy.idle(doWork());
-     * }
-     * 
-     * 
- * - * @param workCount performed in last duty cycle. - */ - override suspend fun idle(workCount: Int) { - if (workCount > 0) { - reset() - } else { - idle() - } - } - - /** - * Perform current idle action (e.g. nothing/yield/sleep). To be used in conjunction with - * {@link IdleStrategy#reset()} to clear internal state when idle period is over (or before it begins). - * Callers are expected to follow this pattern: - * - *
-     * 
-     * while (isRunning)
-     * {
-     *   if (!hasWork())
-     *   {
-     *     idleStrategy.reset();
-     *     while (!hasWork())
-     *     {
-     *       if (!isRunning)
-     *       {
-     *         return;
-     *       }
-     *       idleStrategy.idle();
-     *     }
-     *   }
-     *   doWork();
-     * }
-     * 
-     * 
- */ - override suspend fun idle() { - when (state) { - NOT_IDLE -> { - state = SPINNING - spins++ - } - - SPINNING -> { - ThreadHints.onSpinWait() - if (++spins > maxSpins) { - state = YIELDING - yields = 0 - } - } - - YIELDING -> if (++yields > maxYields) { - state = PARKING - parkPeriodMs = minParkPeriodMs - } else { - yield() - } - - PARKING -> { - delay(parkPeriodMs) - // double the delay until we get to MAX - parkPeriodMs = (parkPeriodMs shl 1).coerceAtMost(maxParkPeriodMs) - } - } - } - - - /** - * Reset the internal state in preparation for entering an idle state again. - */ - override fun reset() { - spins = 0 - yields = 0 - parkPeriodMs = minParkPeriodMs - state = NOT_IDLE - } - - /** - * Simple name by which the strategy can be identified. - * - * @return simple name by which the strategy can be identified. - */ - override fun alias(): String { - return ALIAS - } - - /** - * Creates a clone of this IdleStrategy - */ - override fun clone(): CoroutineBackoffIdleStrategy { - return CoroutineBackoffIdleStrategy(maxSpins = maxSpins, maxYields = maxYields, minParkPeriodMs = minParkPeriodMs, maxParkPeriodMs = maxParkPeriodMs) - } - - /** - * Creates a clone of this IdleStrategy - */ - override fun cloneToNormal(): BackoffIdleStrategy { - return BackoffIdleStrategy(maxSpins, maxYields, minParkPeriodMs, maxParkPeriodMs) - } - - override fun toString(): String { - return "BackoffIdleStrategy{" + - "alias=" + ALIAS + - ", maxSpins=" + maxSpins + - ", maxYields=" + maxYields + - ", minParkPeriodMs=" + minParkPeriodMs + - ", maxParkPeriodMs=" + maxParkPeriodMs + - '}' - } -} diff --git a/src/dorkbox/network/aeron/CoroutineIdleStrategy.kt b/src/dorkbox/network/aeron/CoroutineIdleStrategy.kt deleted file mode 100644 index 44ba436e..00000000 --- a/src/dorkbox/network/aeron/CoroutineIdleStrategy.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2014-2020 Real Logic Limited. - * - * 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. - */ -package dorkbox.network.aeron - -import org.agrona.concurrent.IdleStrategy - -/** - * Idle strategy for use by threads when they do not have work to do. - * - * - * **Note regarding implementor state** - * - * - * Some implementations are known to be stateful, please note that you cannot safely assume implementations to be - * stateless. Where implementations are stateful it is recommended that implementation state is padded to avoid false - * sharing. - * - * - * **Note regarding potential for TTSP(Time To Safe Point) issues** - * - * - * If the caller spins in a 'counted' loop, and the implementation does not include a a safepoint poll this may cause a - * TTSP (Time To SafePoint) problem. If this is the case for your application you can solve it by preventing the idle - * method from being inlined by using a Hotspot compiler command as a JVM argument e.g: - * `-XX:CompileCommand=dontinline,org.agrona.concurrent.NoOpIdleStrategy::idle` - */ -interface CoroutineIdleStrategy { - /** - * Perform current idle action (e.g. nothing/yield/sleep). This method signature expects users to call into it on - * every work 'cycle'. The implementations may use the indication "workCount > 0" to reset internal backoff - * state. This method works well with 'work' APIs which follow the following rules: - *
    - *
  • 'work' returns a value larger than 0 when some work has been done
  • - *
  • 'work' returns 0 when no work has been done
  • - *
  • 'work' may return error codes which are less than 0, but which amount to no work has been done
  • - *
- *

- * Callers are expected to follow this pattern: - * - *

-     * 
-     * while (isRunning)
-     * {
-     *     idleStrategy.idle(doWork());
-     * }
-     * 
-     * 
- * - * @param workCount performed in last duty cycle. - */ - suspend fun idle(workCount: Int) - - /** - * Perform current idle action (e.g. nothing/yield/sleep). To be used in conjunction with - * {@link IdleStrategy#reset()} to clear internal state when idle period is over (or before it begins). - * Callers are expected to follow this pattern: - * - *
-     * 
-     * while (isRunning)
-     * {
-     *   if (!hasWork())
-     *   {
-     *     idleStrategy.reset();
-     *     while (!hasWork())
-     *     {
-     *       if (!isRunning)
-     *       {
-     *         return;
-     *       }
-     *       idleStrategy.idle();
-     *     }
-     *   }
-     *   doWork();
-     * }
-     * 
-     * 
- */ - suspend fun idle() - - /** - * Reset the internal state in preparation for entering an idle state again. - */ - fun reset() - - /** - * Simple name by which the strategy can be identified. - * - * @return simple name by which the strategy can be identified. - */ - fun alias(): String { - return "" - } - - /** - * Creates a clone of this IdleStrategy - */ - fun clone(): CoroutineIdleStrategy - - /** - * Creates a clone of this IdleStrategy - */ - fun cloneToNormal(): IdleStrategy -} diff --git a/src/dorkbox/network/aeron/CoroutineSleepingMillisIdleStrategy.kt b/src/dorkbox/network/aeron/CoroutineSleepingMillisIdleStrategy.kt deleted file mode 100644 index c62345a1..00000000 --- a/src/dorkbox/network/aeron/CoroutineSleepingMillisIdleStrategy.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2014-2020 Real Logic Limited. - * - * 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. - */ -package dorkbox.network.aeron - -import kotlinx.coroutines.delay -import org.agrona.concurrent.SleepingMillisIdleStrategy - -/** - * When idle this strategy is to sleep for a specified period time in milliseconds. - * - * - * This class uses [Coroutine.delay] to idle. - */ -class CoroutineSleepingMillisIdleStrategy : CoroutineIdleStrategy { - companion object { - /** - * Name to be returned from [.alias]. - */ - const val ALIAS = "sleep-ms" - - /** - * Default sleep period when the default constructor is used. - */ - const val DEFAULT_SLEEP_PERIOD_MS = 1L - } - - private val sleepPeriodMs: Long - - /** - * Default constructor that uses [.DEFAULT_SLEEP_PERIOD_MS]. - */ - constructor() { - sleepPeriodMs = DEFAULT_SLEEP_PERIOD_MS - } - - /** - * Constructed a new strategy that will sleep for a given period when idle. - * - * @param sleepPeriodMs period in milliseconds for which the strategy will sleep when work count is 0. - */ - constructor(sleepPeriodMs: Long) { - this.sleepPeriodMs = sleepPeriodMs - } - - /** - * {@inheritDoc} - */ - override suspend fun idle(workCount: Int) { - if (workCount > 0) { - return - } - delay(sleepPeriodMs) - } - - /** - * {@inheritDoc} - */ - override suspend fun idle() { - delay(sleepPeriodMs) - } - - /** - * {@inheritDoc} - */ - override fun reset() {} - - /** - * {@inheritDoc} - */ - override fun alias(): String { - return ALIAS - } - - /** - * Creates a clone of this IdleStrategy - */ - override fun clone(): CoroutineSleepingMillisIdleStrategy { - return CoroutineSleepingMillisIdleStrategy(sleepPeriodMs = sleepPeriodMs) - } - - /** - * Creates a clone of this IdleStrategy - */ - override fun cloneToNormal(): SleepingMillisIdleStrategy { - return SleepingMillisIdleStrategy(sleepPeriodMs) - } - - - override fun toString(): String { - return "SleepingMillisIdleStrategy{" + - "alias=" + ALIAS + - ", sleepPeriodMs=" + sleepPeriodMs + - '}' - } -} diff --git a/src/dorkbox/network/aeron/mediaDriver/MediaDriverServer.kt b/src/dorkbox/network/aeron/EventActionOperator.kt similarity index 54% rename from src/dorkbox/network/aeron/mediaDriver/MediaDriverServer.kt rename to src/dorkbox/network/aeron/EventActionOperator.kt index 87f125ea..446816e5 100644 --- a/src/dorkbox/network/aeron/mediaDriver/MediaDriverServer.kt +++ b/src/dorkbox/network/aeron/EventActionOperator.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:Suppress("DuplicatedCode") -package dorkbox.network.aeron.mediaDriver +package dorkbox.network.aeron -import io.aeron.Subscription - -abstract class MediaDriverServer(val port: Int, - val streamId: Int, - val sessionId: Int, - val connectionTimeoutSec: Int, val - isReliable: Boolean) : MediaDriverConnection { - - lateinit var subscription: Subscription +internal interface EventActionOperator { + operator fun invoke(): Int } diff --git a/src/dorkbox/network/aeron/EventCloseOperator.kt b/src/dorkbox/network/aeron/EventCloseOperator.kt new file mode 100644 index 00000000..0110a068 --- /dev/null +++ b/src/dorkbox/network/aeron/EventCloseOperator.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.aeron + +internal interface EventCloseOperator { + operator fun invoke() +} diff --git a/src/dorkbox/network/aeron/EventPoller.kt b/src/dorkbox/network/aeron/EventPoller.kt new file mode 100644 index 00000000..65ccf8a5 --- /dev/null +++ b/src/dorkbox/network/aeron/EventPoller.kt @@ -0,0 +1,241 @@ +/* + * Copyright 2024 dorkbox, llc + * + * 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 dorkbox.network.aeron + +import dorkbox.bytes.ByteArrayWrapper +import dorkbox.collections.ConcurrentIterator +import dorkbox.network.Configuration +import dorkbox.network.connection.EndPoint +import dorkbox.util.NamedThreadFactory +import kotlinx.atomicfu.atomic +import org.agrona.concurrent.IdleStrategy +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.concurrent.* +import java.util.concurrent.locks.* +import kotlin.concurrent.write + +/** + * there are threading issues if there are client(s) and server's within the same JVM, where we have thread starvation + * + * additionally, if we have MULTIPLE clients on the same machine, we are limited by the CPU core count. Ideally we want to share + * this among ALL clients within the same JVM so that we can support multiple clients/servers + */ +internal class EventPoller { + + private class EventAction(val onAction: EventActionOperator, val onClose: EventCloseOperator) + + companion object { + internal const val REMOVE = -1 + + val eventLogger = LoggerFactory.getLogger(EventPoller::class.java.simpleName) + + + private val pollExecutor = Executors.newSingleThreadExecutor( + NamedThreadFactory("Poll Dispatcher", Configuration.networkThreadGroup, true) + ) + } + + private var configured = false + + private lateinit var pollStrategy: IdleStrategy + + @Volatile + private var running = false + private var lock = ReentrantReadWriteLock() + + // this is thread safe + private val pollEvents = ConcurrentIterator() + private val submitEvents = atomic(0) + private val configureEventsEndpoints = mutableSetOf() + + @Volatile + private var shutdownLatch = CountDownLatch(0) + + + @Volatile + private var threadId = 0L + + + fun isDispatch(): Boolean { + // this only works because we are a single thread dispatch + return threadId == Thread.currentThread().id + } + + fun configure(logger: Logger, config: Configuration, endPoint: EndPoint<*>) { + lock.write { + if (logger.isDebugEnabled) { + logger.debug("Initializing the Network Event Poller...") + } + configureEventsEndpoints.add(ByteArrayWrapper.wrap(endPoint.storage.publicKey)) + + if (!configured) { + if (logger.isTraceEnabled) { + logger.trace("Configuring the Network Event Poller...") + } + + running = true + configured = true + shutdownLatch = CountDownLatch(1) + pollStrategy = config.pollIdleStrategy + + pollExecutor.submit { + val pollIdleStrategy = pollStrategy + var pollCount = 0 + threadId = Thread.currentThread().id // only ever 1 thread!!! + + pollIdleStrategy.reset() + + while (running) { + pollEvents.forEachRemovable { + try { + // check to see if we should remove this event (when a client/server closes, it is removed) + // once ALL endpoint are closed, this is shutdown. + val poll = it.onAction() + + // <0 means we remove the event from processing + // 0 means we idle + // >0 means reset and don't idle (because there are likely more poll events) + if (poll < 0) { + // remove our event, it is no longer valid + pollEvents.remove(this) + it.onClose() // shutting down + } else if (poll > 0) { + pollCount += poll + } + } catch (e: Exception) { + eventLogger.error("Unexpected error during Network Event Polling! Aborting event dispatch for it!", e) + + // remove our event, it is no longer valid + pollEvents.remove(this) + it.onClose() // shutting down + } + } + + pollIdleStrategy.idle(pollCount) + } + + + // now we have to REMOVE all poll events -- so that their remove logic will run. + pollEvents.forEachRemovable { + // remove our event, it is no longer valid + pollEvents.remove(this) + it.onClose() // shutting down + } + + shutdownLatch.countDown() + } + } else { + // we don't want to use .equals, because that also compares STATE, which for us is going to be different because we are cloned! + // toString has the right info to compare types/config accurately + require(pollStrategy.toString() == config.pollIdleStrategy.toString()) { + "The network event poll strategy is different between the multiple instances of network clients/servers. There **WILL BE** thread starvation, so this behavior is forbidden!" + } + } + } + } + + /** + * Will cause the executing thread to wait until the event has been started + */ + fun submit(action: EventActionOperator, onClose: EventCloseOperator) = lock.write { + submitEvents.getAndIncrement() + + // this forces the current thread to WAIT until the network poll system has started + val pollStartupLatch = CountDownLatch(1) + + pollEvents.add(EventAction(action, onClose)) + pollEvents.add(EventAction( + object : EventActionOperator { + override fun invoke(): Int { + pollStartupLatch.countDown() + + // remove ourselves + return REMOVE + } + } + , object : EventCloseOperator { + override fun invoke() {} + } + )) + + pollStartupLatch.await() + + submitEvents.getAndDecrement() + } + + + + /** + * Waits for all events to finish running + */ + fun close(logger: Logger, endPoint: EndPoint<*>) { + // make sure that we close on the CLOSE dispatcher if we run on the poll dispatcher! + if (isDispatch()) { + endPoint.eventDispatch.CLOSE.launch { + close(logger, endPoint) + } + return + } + + lock.write { + logger.debug("Requesting close for the Network Event Poller...") + + // ONLY if there are no more poll-events do we ACTUALLY shut down. + // when an endpoint closes its polling, it will automatically be removed from this datastructure. + val publicKeyWrapped = ByteArrayWrapper.wrap(endPoint.storage.publicKey) + configureEventsEndpoints.removeIf { it == publicKeyWrapped } + val cEvents = configureEventsEndpoints.size + + // these prevent us from closing too early + val pEvents = pollEvents.size() + val sEvents = submitEvents.value + + if (running && sEvents == 0 && cEvents == 0) { + when (pEvents) { + 0 -> { + logger.debug("Closing the Network Event Poller...") + doClose(logger) + } + else -> { + if (logger.isDebugEnabled) { + logger.debug("Not closing the Network Event Poller... (isRunning=$running submitEvents=$sEvents configureEvents=${cEvents} pollEvents=$pEvents)") + } + } + } + } else if (logger.isDebugEnabled) { + logger.debug("Not closing the Network Event Poller... (isRunning=$running submitEvents=$sEvents configureEvents=${cEvents} pollEvents=$pEvents)") + } + } + } + + private fun doClose(logger: Logger) { + val wasRunning = running + + running = false + while (!shutdownLatch.await(500, TimeUnit.MILLISECONDS)) { + logger.error("Waiting for Network Event Poller to close. It should not take this long") + } + configured = false + + if (wasRunning) { + pollExecutor.awaitTermination(200, TimeUnit.MILLISECONDS) + } + + logger.debug("Closed Network Event Poller: wasRunning=$wasRunning") + } +} diff --git a/src/dorkbox/network/aeron/FixTransportPoller.kt b/src/dorkbox/network/aeron/FixTransportPoller.kt new file mode 100644 index 00000000..ce208c89 --- /dev/null +++ b/src/dorkbox/network/aeron/FixTransportPoller.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.aeron + +import dorkbox.jna.ClassUtils +import dorkbox.os.OS +import javassist.ClassPool +import javassist.CtNewMethod + + + +object FixTransportPoller { + // allow access to sun.nio.ch.SelectorImpl without causing reflection or JPMS module issues + fun init() { + if (OS.javaVersion <= 11) { + // older versions of java don't need to worry about rewriting anything + return + } + + + try { + val pool = ClassPool.getDefault() + + run { + val dynamicClass = pool.makeClass("sun.nio.ch.SelectorImplAccessory") + val method = CtNewMethod.make( + ("public static java.lang.reflect.Field getKey(java.lang.String fieldName) { " + + "java.lang.reflect.Field field = Class.forName(\"sun.nio.ch.SelectorImpl\").getDeclaredField( fieldName );" + + "field.setAccessible( true );" + + "return field;" + + "}"), dynamicClass + ) + dynamicClass.addMethod(method) + + val dynamicClassBytes = dynamicClass.toBytecode() + ClassUtils.defineClass(null, dynamicClassBytes) + } + + // have to trampoline off this to get around module access + run { + val dynamicClass = pool.makeClass("java.lang.SelectorImplAccessory") + val method = CtNewMethod.make( + ("public static java.lang.reflect.Field getKey(java.lang.String fieldName) { " + + "return sun.nio.ch.SelectorImplAccessory.getKey(fieldName);" + + "}"), dynamicClass + ) + dynamicClass.addMethod(method) + + val dynamicClassBytes = dynamicClass.toBytecode() + ClassUtils.defineClass(null, dynamicClassBytes) + } + + run { + val dynamicClass = pool.getCtClass("org.agrona.nio.TransportPoller") + + // Get the static initializer + val staticInitializer = dynamicClass.classInitializer + + // Remove the existing static initializer + dynamicClass.removeConstructor(staticInitializer) + + val initializer = dynamicClass.makeClassInitializer() + initializer.insertAfter( + "java.lang.System.err.println(\"updating TransportPoller!\");" + + "java.lang.reflect.Field selectKeysField = null;\n" + + "java.lang.reflect.Field publicSelectKeysField = null;\n" + + "try {\n" + + " java.nio.channels.Selector selector = java.nio.channels.Selector.open();\n" + + " Throwable var3 = null;\n" + "\n" + + " try {\n" + + " Class clazz = Class.forName(\"sun.nio.ch.SelectorImpl\", false, ClassLoader.getSystemClassLoader());\n" + + " if (clazz.isAssignableFrom(selector.getClass())) {\n" + + " selectKeysField = java.lang.SelectorImplAccessory.getKey(\"selectedKeys\");\n" + + " publicSelectKeysField = java.lang.SelectorImplAccessory.getKey(\"publicSelectedKeys\");\n" + + " }\n" + + " } catch (Throwable var21) {\n" + + " var3 = var21;\n" + + " throw var21;\n" + + " } finally {\n" + + " if (selector != null) {\n" + + " if (var3 != null) {\n" + + " try {\n" + + " selector.close();\n" + + " } catch (Throwable var20) {\n" + + " var3.addSuppressed(var20);\n" + + " }\n" + + " } else {\n" + + " selector.close();\n" + + " }\n" + + " }\n" + + " }\n" + + "} catch (Exception var23) {\n" + + " org.agrona.LangUtil.rethrowUnchecked(var23);\n" + + "} finally {\n" + + " org.agrona.nio.TransportPoller.SELECTED_KEYS_FIELD = selectKeysField;\n" + + " org.agrona.nio.TransportPoller.PUBLIC_SELECTED_KEYS_FIELD = publicSelectKeysField;\n" + + "}" + ) + + + // perform pre-verification for the modified method + initializer.methodInfo.rebuildStackMapForME(pool) + + val dynamicClassBytes = dynamicClass.toBytecode() + ClassUtils.defineClass(ClassLoader.getSystemClassLoader(), dynamicClassBytes) + } + + } catch (e: Exception) { + throw RuntimeException("Could not fix Aeron TransportPoller", e) + } + } +} diff --git a/src/dorkbox/network/aeron/mediaDriver/ClientIpcDriver.kt b/src/dorkbox/network/aeron/mediaDriver/ClientIpcDriver.kt deleted file mode 100644 index 01399152..00000000 --- a/src/dorkbox/network/aeron/mediaDriver/ClientIpcDriver.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2021 dorkbox, llc - * - * 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 dorkbox.network.aeron.mediaDriver - -import dorkbox.network.aeron.AeronDriver -import dorkbox.network.aeron.mediaDriver.MediaDriverConnection.Companion.uri -import dorkbox.network.connection.ListenerManager -import dorkbox.network.exceptions.ClientRetryException -import dorkbox.network.exceptions.ClientTimedOutException -import mu.KLogger -import java.lang.Thread.sleep -import java.util.concurrent.* - -/** - * For a client, the streamId specified here MUST be manually flipped because they are in the perspective of the SERVER - * NOTE: IPC connection will ALWAYS have a timeout of 10 second to connect. This is IPC, it should connect fast - */ -internal open class ClientIpcDriver(streamId: Int, - sessionId: Int, - localSessionId: Int) : - MediaDriverClient( - port = streamId, - streamId = streamId, - remoteSessionId = sessionId, - localSessionId = localSessionId, - connectionTimeoutSec = 10, - isReliable = true - ) { - - var success: Boolean = false - override val type = "ipc" - - override val subscriptionPort: Int = localSessionId - - /** - * Set up the subscription + publication channels to the server - * - * @throws ClientRetryException if we need to retry to connect - * @throws ClientTimedOutException if we cannot connect to the server in the designated time - */ - fun build(aeronDriver: AeronDriver, logger: KLogger) { - // Create a publication at the given address and port, using the given stream ID. - // Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs. - val publicationUri = uri("ipc", remoteSessionId) - - // Create a subscription at the given address and port, using the given stream ID. - val subscriptionUri = uri("ipc", 0) - - if (logger.isTraceEnabled) { - logger.trace("IPC client pub URI: ${publicationUri.build()}") - logger.trace("IPC server sub URI: ${subscriptionUri.build()}") - } - - var success = false - - // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe - // publication of any state to other threads and not be long running or re-entrant with the client. - - // For publications, if we add them "too quickly" (faster than the 'linger' timeout), Aeron will throw exceptions. - // ESPECIALLY if it is with the same streamID - // this check is in the "reconnect" logic - - val publication = aeronDriver.addPublication(publicationUri, streamId) - val subscription = aeronDriver.addSubscription(subscriptionUri, localSessionId) - - - // always include the linger timeout, so we don't accidentally kill ourself by taking too long - val timoutInNanos = TimeUnit.SECONDS.toNanos(connectionTimeoutSec.toLong()) + aeronDriver.getLingerNs() - val startTime = System.nanoTime() - - while (System.nanoTime() - startTime < timoutInNanos) { - if (publication.isConnected) { - success = true - break - } - - sleep(500L) - } - if (!success) { - subscription.close() - publication.close() - - val clientTimedOutException = ClientTimedOutException("Cannot create publication IPC connection to server") - ListenerManager.cleanAllStackTrace(clientTimedOutException) - throw clientTimedOutException - } - - this.success = true - this.subscription = subscription - this.publication = publication - } - - override val info : String by lazy { - if (sessionId != AeronDriver.RESERVED_SESSION_ID_INVALID) { - "[$sessionId] IPC connection established to [$streamId|$subscriptionPort]" - } else { - "Connecting handshake to IPC [$streamId|$subscriptionPort]" - } - } - - override fun toString(): String { - return info - } -} diff --git a/src/dorkbox/network/aeron/mediaDriver/ClientUdpDriver.kt b/src/dorkbox/network/aeron/mediaDriver/ClientUdpDriver.kt deleted file mode 100644 index da12dfbe..00000000 --- a/src/dorkbox/network/aeron/mediaDriver/ClientUdpDriver.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2021 dorkbox, llc - * - * 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 dorkbox.network.aeron.mediaDriver - -import dorkbox.netUtil.IPv4 -import dorkbox.netUtil.IPv6 -import dorkbox.network.aeron.AeronDriver -import dorkbox.network.aeron.mediaDriver.MediaDriverConnection.Companion.uriEndpoint -import dorkbox.network.connection.ListenerManager -import dorkbox.network.exceptions.ClientRetryException -import dorkbox.network.exceptions.ClientTimedOutException -import mu.KLogger -import java.lang.Thread.sleep -import java.net.Inet4Address -import java.net.Inet6Address -import java.net.InetAddress -import java.util.concurrent.* - -/** - * For a client, the ports specified here MUST be manually flipped because they are in the perspective of the SERVER. - * A connection timeout of 0, means to wait forever - */ -internal class ClientUdpDriver(val address: InetAddress, val addressString: String, - port: Int, - streamId: Int, - sessionId: Int, - localSessionId: Int, - connectionTimeoutSec: Int = 0, - isReliable: Boolean) : - MediaDriverClient(port, streamId, sessionId, localSessionId, connectionTimeoutSec, isReliable) { - - var success: Boolean = false - override val type: String by lazy { - if (address is Inet4Address) { - "IPv4" - } else { - "IPv6" - } - } - - override val subscriptionPort: Int by lazy { - val addressesAndPorts = subscription.localSocketAddresses() - val first = addressesAndPorts.first() - - // split - val splitPoint = first.lastIndexOf(':') - val port = first.substring(splitPoint+1) - port.toInt() - } - - /** - * @throws ClientRetryException if we need to retry to connect - * @throws ClientTimedOutException if we cannot connect to the server in the designated time - */ - @Suppress("DuplicatedCode") - fun build(aeronDriver: AeronDriver, logger: KLogger) { - var success = false - - // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe - // publication of any state to other threads and not be long running or re-entrant with the client. - - // on close, the publication CAN linger (in case a client goes away, and then comes back) - // AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) - - // Create a publication at the given address and port, using the given stream ID. - // Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs. - val publicationUri = uriEndpoint("udp", remoteSessionId, isReliable, address, addressString, port) - logger.trace("client pub URI: $type ${publicationUri.build()}") - - // For publications, if we add them "too quickly" (faster than the 'linger' timeout), Aeron will throw exceptions. - // ESPECIALLY if it is with the same streamID. This was noticed as a problem with IPC - val publication = aeronDriver.addPublication(publicationUri, streamId) - - - val localAddresses = publication.localSocketAddresses().first() - // split - val splitPoint = localAddresses.lastIndexOf(':') - val localAddressString = localAddresses.substring(0, splitPoint) - - - // the subscription here is WILDCARD - val localAddress = if (address is Inet6Address) { - IPv6.toAddress(localAddressString)!! - } else { - IPv4.toAddress(localAddressString)!! - } - - // Create a subscription the given address and port, using the given stream ID. - val subscriptionUri = uriEndpoint("udp", localSessionId, isReliable, localAddress, localAddressString, 0) - logger.trace("client sub URI: $type ${subscriptionUri.build()}") - - val subscription = aeronDriver.addSubscription(subscriptionUri, streamId) - - // always include the linger timeout, so we don't accidentally kill ourself by taking too long - val timoutInNanos = TimeUnit.SECONDS.toNanos(connectionTimeoutSec.toLong()) + aeronDriver.getLingerNs() - val startTime = System.nanoTime() - - while (System.nanoTime() - startTime < timoutInNanos) { - if (publication.isConnected) { - success = true - break - } - - sleep(500L) - } - - if (!success) { - subscription.close() - publication.close() - - val ex = ClientTimedOutException("Cannot create publication to $type $addressString in $connectionTimeoutSec seconds") - ListenerManager.cleanAllStackTrace(ex) - throw ex - } - - this.success = true - this.publication = publication - this.subscription = subscription - } - - override val info: String by lazy { - if (sessionId != AeronDriver.RESERVED_SESSION_ID_INVALID) { - "$addressString [$port|$subscriptionPort] [$streamId|$sessionId] (reliable:$isReliable)" - } else { - "Connecting handshake to $addressString [$port|$subscriptionPort] [$streamId|*] (reliable:$isReliable)" - } - } - - override fun toString(): String { - return info - } -} diff --git a/src/dorkbox/network/aeron/mediaDriver/MediaDriverClient.kt b/src/dorkbox/network/aeron/mediaDriver/MediaDriverClient.kt deleted file mode 100644 index e15bac3b..00000000 --- a/src/dorkbox/network/aeron/mediaDriver/MediaDriverClient.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 dorkbox, llc - * - * 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. - */ -@file:Suppress("DuplicatedCode") - -package dorkbox.network.aeron.mediaDriver - -import io.aeron.Publication -import io.aeron.Subscription - -abstract class MediaDriverClient(val port: Int, - val streamId: Int, - val remoteSessionId: Int, - val localSessionId: Int, - val connectionTimeoutSec: Int, - val isReliable: Boolean) : MediaDriverConnection { - - lateinit var subscription: Subscription - lateinit var publication: Publication - - abstract val subscriptionPort: Int -} diff --git a/src/dorkbox/network/aeron/mediaDriver/MediaDriverConnectInfo.kt b/src/dorkbox/network/aeron/mediaDriver/MediaDriverConnectInfo.kt deleted file mode 100644 index 308810b8..00000000 --- a/src/dorkbox/network/aeron/mediaDriver/MediaDriverConnectInfo.kt +++ /dev/null @@ -1,16 +0,0 @@ -package dorkbox.network.aeron.mediaDriver - -import io.aeron.Publication -import io.aeron.Subscription -import java.net.InetAddress - -data class MediaDriverConnectInfo(val subscription: Subscription, - val publication: Publication, - val subscriptionPort: Int, - val publicationPort: Int, - val streamId: Int, - val sessionId: Int, - val isReliable: Boolean, - val remoteAddress: InetAddress?, - val remoteAddressString: String, -) diff --git a/src/dorkbox/network/aeron/mediaDriver/MediaDriverConnection.kt b/src/dorkbox/network/aeron/mediaDriver/MediaDriverConnection.kt deleted file mode 100644 index 7db76477..00000000 --- a/src/dorkbox/network/aeron/mediaDriver/MediaDriverConnection.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 dorkbox, llc - * - * 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. - */ -@file:Suppress("DuplicatedCode") - -package dorkbox.network.aeron.mediaDriver - -import dorkbox.network.aeron.AeronDriver -import io.aeron.ChannelUriStringBuilder -import java.net.Inet4Address -import java.net.InetAddress - -interface MediaDriverConnection { - - val type: String - - // We don't use 'suspend' for these, because we have to pump events from a NORMAL thread. If there are any suspend points, there is - // the potential for a live-lock due to coroutine scheduling - val info : String - - companion object { - fun uri(type: String, sessionId: Int, isReliable: Boolean? = null): ChannelUriStringBuilder { - val builder = ChannelUriStringBuilder().media(type) - if (isReliable != null) { - builder.reliable(isReliable) - } - - if (sessionId != AeronDriver.RESERVED_SESSION_ID_INVALID) { - builder.sessionId(sessionId) - } - - return builder - } - - fun uriEndpoint(type: String, sessionId: Int, isReliable: Boolean, address: InetAddress, addressString: String, port: Int): ChannelUriStringBuilder { - val builder = uri(type, sessionId, isReliable) - - if (address is Inet4Address) { - builder.endpoint("$addressString:$port") - } else { - // IPv6 requires the address to be bracketed by [...] - if (addressString[0] == '[') { - builder.endpoint("$addressString:$port") - } else { - // there MUST be [] surrounding the IPv6 address for aeron to like it! - builder.endpoint("[$addressString]:$port") - } - } - - return builder - } - } -} diff --git a/src/dorkbox/network/aeron/mediaDriver/ServerIpcDriver.kt b/src/dorkbox/network/aeron/mediaDriver/ServerIpcDriver.kt deleted file mode 100644 index c91fdb0c..00000000 --- a/src/dorkbox/network/aeron/mediaDriver/ServerIpcDriver.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2021 dorkbox, llc - * - * 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 dorkbox.network.aeron.mediaDriver - -import dorkbox.network.aeron.AeronDriver -import dorkbox.network.aeron.mediaDriver.MediaDriverConnection.Companion.uri -import mu.KLogger - -/** - * For a client, the streamId specified here MUST be manually flipped because they are in the perspective of the SERVER - * NOTE: IPC connection will ALWAYS have a timeout of 10 second to connect. This is IPC, it should connect fast - */ -internal open class ServerIpcDriver(streamId: Int, - sessionId: Int) : - MediaDriverServer(0, streamId, sessionId, 10, true) { - - - var success: Boolean = false - override val type = "ipc" - - /** - * Setup the subscription + publication channels on the server. - * - * serverAddress is ignored for IPC - */ - fun build(aeronDriver: AeronDriver, logger: KLogger) { - // Create a subscription at the given address and port, using the given stream ID. - val subscriptionUri = uri("ipc", sessionId) - - if (logger.isTraceEnabled) { - logger.trace("IPC server sub URI: ${subscriptionUri.build()}") - } - - success = true - subscription = aeronDriver.addSubscription(subscriptionUri, streamId) - } - - override val info : String by lazy { - if (sessionId != AeronDriver.RESERVED_SESSION_ID_INVALID) { - "[$sessionId] IPC listening on [$streamId] [$sessionId]" - } else { - "Listening handshake on IPC [$streamId] [$sessionId]" - } - } - - override fun toString(): String { - return info - } -} diff --git a/src/dorkbox/network/aeron/mediaDriver/ServerUdpDriver.kt b/src/dorkbox/network/aeron/mediaDriver/ServerUdpDriver.kt deleted file mode 100644 index 4d38b2cf..00000000 --- a/src/dorkbox/network/aeron/mediaDriver/ServerUdpDriver.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2021 dorkbox, llc - * - * 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 dorkbox.network.aeron.mediaDriver - -import dorkbox.netUtil.IP -import dorkbox.netUtil.IPv4 -import dorkbox.netUtil.IPv6 -import dorkbox.network.aeron.AeronDriver -import dorkbox.network.aeron.mediaDriver.MediaDriverConnection.Companion.uriEndpoint -import mu.KLogger -import java.net.Inet4Address -import java.net.InetAddress - -/** - * For a client, the ports specified here MUST be manually flipped because they are in the perspective of the SERVER. - * A connection timeout of 0, means to wait forever - */ -internal open class ServerUdpDriver(val listenAddress: InetAddress, - port: Int, - streamId: Int, - sessionId: Int, - connectionTimeoutSec: Int, - isReliable: Boolean) : - MediaDriverServer(port, streamId, sessionId, connectionTimeoutSec, isReliable) { - - - var success: Boolean = false - override val type = "udp" - - fun build(aeronDriver: AeronDriver, logger: KLogger) { - // Create a subscription at the given address and port, using the given stream ID. - val subscriptionUri = uriEndpoint("udp", sessionId, isReliable, listenAddress, IP.toString(listenAddress), port) - - if (logger.isTraceEnabled) { - if (listenAddress is Inet4Address) { - logger.trace("IPV4 server sub URI: ${subscriptionUri.build()}") - } else { - logger.trace("IPV6 server sub URI: ${subscriptionUri.build()}") - } - } - - this.success = true - this.subscription = aeronDriver.addSubscription(subscriptionUri, streamId) - } - - override val info: String by lazy { - val address = if (listenAddress == IPv4.WILDCARD || listenAddress == IPv6.WILDCARD) { - if (listenAddress == IPv4.WILDCARD) { - listenAddress.hostAddress - } else { - IPv4.WILDCARD.hostAddress + "/" + listenAddress.hostAddress - } - } else { - IP.toString(listenAddress) - } - - if (sessionId != AeronDriver.RESERVED_SESSION_ID_INVALID) { - "Listening on $address [$port] [$streamId|$sessionId] (reliable:$isReliable)" - } else { - "Listening handshake on $address [$port] [$streamId|*] (reliable:$isReliable)" - } - } - - override fun toString(): String { - return info - } -} diff --git a/src/dorkbox/network/aeron/mediaDriver/UdpMediaDriverPairedConnection.kt b/src/dorkbox/network/aeron/mediaDriver/UdpMediaDriverPairedConnection.kt deleted file mode 100644 index a98dd8a0..00000000 --- a/src/dorkbox/network/aeron/mediaDriver/UdpMediaDriverPairedConnection.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2021 dorkbox, llc - * - * 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 dorkbox.network.aeron.mediaDriver - -import io.aeron.Publication -import java.net.InetAddress - -/** - * This represents the connection PAIR between a server<->client - * A connection timeout of 0, means to wait forever - */ -internal class UdpMediaDriverPairedConnection( - listenAddress: InetAddress, - val remoteAddress: InetAddress, - val remoteAddressString: String, - val publicationPort: Int, - subscriptionPort: Int, - streamId: Int, - sessionId: Int, - connectionTimeoutSec: Int, - isReliable: Boolean, - val publication: Publication -) : - ServerUdpDriver(listenAddress, subscriptionPort, streamId, sessionId, connectionTimeoutSec, isReliable) { - - override fun toString(): String { - return "$remoteAddressString [$port|$publicationPort] [$streamId|$sessionId] (reliable:$isReliable)" - } -} diff --git a/src/dorkbox/network/aeron/package-info.java b/src/dorkbox/network/aeron/package-info.java new file mode 100644 index 00000000..07aee188 --- /dev/null +++ b/src/dorkbox/network/aeron/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.aeron; diff --git a/src/dorkbox/network/connection/Connection.kt b/src/dorkbox/network/connection/Connection.kt index a2ad0c66..0171cb45 100644 --- a/src/dorkbox/network/connection/Connection.kt +++ b/src/dorkbox/network/connection/Connection.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,61 +15,102 @@ */ package dorkbox.network.connection -import dorkbox.network.handshake.ConnectionCounts -import dorkbox.network.handshake.RandomId65kAllocator +import dorkbox.network.Client +import dorkbox.network.Server +import dorkbox.network.aeron.AeronDriver.Companion.sessionIdAllocator +import dorkbox.network.aeron.AeronDriver.Companion.streamIdAllocator +import dorkbox.network.connection.buffer.BufferedMessages +import dorkbox.network.connection.buffer.BufferedSession import dorkbox.network.ping.Ping -import dorkbox.network.ping.PingManager import dorkbox.network.rmi.RmiSupportConnection -import io.aeron.FragmentAssembler -import io.aeron.Publication -import io.aeron.Subscription +import io.aeron.Image +import io.aeron.logbuffer.FragmentHandler import io.aeron.logbuffer.Header +import io.aeron.protocol.DataHeaderFlyweight import kotlinx.atomicfu.atomic import kotlinx.atomicfu.getAndUpdate -import kotlinx.coroutines.runBlocking import org.agrona.DirectBuffer -import java.lang.Thread.sleep -import java.net.InetAddress -import java.util.concurrent.* +import javax.crypto.SecretKey /** - * This connection is established once the registration information is validated, and the various connect/filter checks have passed + * This connection is established once the registration information is validated, and the various connect/filter checks have passed. + * + * Connections are also BUFFERED, meaning that if the connection between a client-server goes down because of a network glitch, then the + * data being sent is not lost (it is buffered) and then re-sent once a new connection has the same UUID within the timout period. + * + * References to the old connection will also redirect to the new connection. */ open class Connection(connectionParameters: ConnectionParams<*>) { - private var messageHandler: FragmentAssembler - internal val subscription: Subscription - internal val publication: Publication + private val messageHandler: FragmentHandler /** - * The publication port (used by aeron) for this connection. This is from the perspective of the server! + * The specific connection details for this connection! + * + * NOTE: remember, the connection details are for the connection, but the toString() info is reversed for the client + * (so that we can line-up client/server connection logs) */ - private val subscriptionPort: Int - private val publicationPort: Int + val info = connectionParameters.connectionInfo /** - * the stream id of this connection. Can be 0 for IPC connections + * the endpoint associated with this connection */ - val streamId: Int + internal val endPoint = connectionParameters.endPoint + + internal val subscription = info.sub + internal val publication = info.pub + private lateinit var image: Image + + // only accessed on a single thread! + private val connectionExpirationTimoutNanos = endPoint.config.connectionExpirationTimoutNanos + // the timeout starts from when the connection is first created, so that we don't get "instant" timeouts when the server rejects a connection + private var connectionTimeoutTimeNanos = System.nanoTime() /** - * the session id of this connection. This value is UNIQUE + * There can be concurrent writes to the network stack, at most 1 per connection. Each connection has its own logic on the remote endpoint, + * and can have its own back-pressure. */ - val id: Int + internal val sendIdleStrategy = endPoint.config.sendIdleStrategy /** - * the remote address, as a string. Will be null for IPC connections + * This is the client UUID. This is useful determine if the same client is connecting multiple times to a server (instead of only using IP address) */ - val remoteAddress: InetAddress? + val uuid = connectionParameters.publicKey /** - * the remote address, as a string. Will be "ipc" for IPC connections + * The unique session id of this connection, assigned by the server. + * + * Specifically this is the subscription session ID for the server */ - val remoteAddressString: String + val id = if (endPoint::class.java == Client::class.java) { + info.sessionIdPub + } else { + info.sessionIdSub + } + + /** + * The tag name for a connection permits an INCOMING client to define a custom string. The max length is 32 + */ + val tag = info.tagName + + /** + * The remote address, as a string. Will be null for IPC connections + */ + val remoteAddress = info.remoteAddress + + /** + * The remote address, as a string. Will be "IPC" for IPC connections + */ + val remoteAddressString = info.remoteAddressString + + /** + * The remote port. Will be 0 for IPC connections + */ + val remotePort = info.portPub /** * @return true if this connection is an IPC connection */ - val isIpc = connectionParameters.connectionInfo.remoteAddress == null + val isIpc = info.isIpc /** * @return true if this connection is a network connection @@ -77,9 +118,26 @@ open class Connection(connectionParameters: ConnectionParams<*>) { val isNetwork = !isIpc /** - * the endpoint associated with this connection + * used when the connection is buffered */ - internal val endPoint = connectionParameters.endPoint + private val bufferedSession: BufferedSession + + /** + * used to determine if this connection will have buffered messages enabled or not. + */ + internal val enableBufferedMessages = connectionParameters.enableBufferedMessages + + /** + * The largest size a SINGLE message via AERON can be. Because the maximum size we can send in a "single fragment" is the + * publication.maxPayloadLength() function (which is the MTU length less header). We could depend on Aeron for fragment reassembly, + * but that has a (very low) maximum reassembly size -- so we have our own mechanism for object fragmentation/assembly, which + * is (in reality) only limited by available ram. + */ + internal val maxMessageSize = if (isNetwork) { + endPoint.config.networkMtuSize - DataHeaderFlyweight.HEADER_LENGTH + } else { + endPoint.config.ipcMtuSize - DataHeaderFlyweight.HEADER_LENGTH + } private val listenerManager = atomic?>(null) @@ -87,105 +145,120 @@ open class Connection(connectionParameters: ConnectionParams<*>) { private val isClosed = atomic(false) - // enableNotifyDisconnect : we don't always want to enable notifications on disconnect - internal var closeAction: suspend () -> Unit = {} - - // only accessed on a single thread! - private var connectionLastCheckTimeNanos = 0L - private var connectionTimeoutTimeNanos = 0L - - // always offset by the linger amount, since we cannot act faster than the linger for adding/removing publications - private val connectionCheckIntervalNanos = connectionParameters.endPoint.config.connectionCheckIntervalNanos + endPoint.aeronDriver.getLingerNs() - private val connectionExpirationTimoutNanos = connectionParameters.endPoint.config.connectionExpirationTimoutNanos + endPoint.aeronDriver.getLingerNs() - // while on the CLIENT, if the SERVER's ecc key has changed, the client will abort and show an error. - private val remoteKeyChanged = connectionParameters.publicKeyValidation == PublicKeyValidationState.TAMPERED - - // The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 8 (external counter) + 4 (GCM counter) - // The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this - // counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small) -// private val aes_gcm_iv = atomic(0) + internal val remoteKeyChanged = connectionParameters.publicKeyValidation == PublicKeyValidationState.TAMPERED /** * Methods supporting Remote Method Invocation and Objects */ val rmi: RmiSupportConnection - // a record of how many messages are in progress of being sent. When closing the connection, this number must be 0 - private val messagesInProgress = atomic(0) - - // we customize the toString() value for this connection, and it's just better to cache it's value (since it's a modestly complex string) + // we customize the toString() value for this connection, and it's just better to cache its value (since it's a modestly complex string) private val toString0: String + /** + * @return the AES key + */ + internal val cryptoKey: SecretKey = connectionParameters.cryptoKey + // The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 4 (external counter) + 4 (GCM counter) + // The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this + // counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small) + internal val aes_gcm_iv = atomic(0) - init { - val connectionInfo = connectionParameters.connectionInfo + // Used to track that this connection WILL be closed, but has not yet been closed. + @Volatile + internal var closeRequested = false - id = connectionInfo.sessionId // NOTE: this is UNIQUE per server! - subscription = connectionInfo.subscription - publication = connectionInfo.publication + init { + // NOTE: subscriptions (ie: reading from buffers, etc) are not thread safe! Because it is ambiguous HOW EXACTLY they are unsafe, + // we exclusively read from the DirectBuffer on a single thread. + + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be: + // - long running + // - re-entrant with the client + messageHandler = FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header -> + // Subscriptions are NOT multi-thread safe, so only processed on the thread that calls .poll()! + endPoint.dataReceive(buffer, offset, length, header, this@Connection) + } - // can only get this AFTER we have built the sub/pub - streamId = connectionInfo.streamId // NOTE: this is UNIQUE per server! - subscriptionPort = connectionInfo.subscriptionPort - publicationPort = connectionInfo.publicationPort + bufferedSession = when (endPoint) { + is Server -> endPoint.bufferedManager.onConnect(this) + is Client -> endPoint.bufferedManager!!.onConnect(this) + else -> throw RuntimeException("Unable to determine type, aborting!") + } - remoteAddress = connectionInfo.remoteAddress - remoteAddressString = connectionInfo.remoteAddressString + @Suppress("LeakingThis") + rmi = endPoint.rmiConnectionSupport.getNewRmiSupport(this) - toString0 = "[${id}/${streamId}] $remoteAddressString [$publicationPort|$subscriptionPort]" + // For toString() and logging + toString0 = info.getLogInfo(logger.isDebugEnabled) + } - messageHandler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header -> - // this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe! + /** + * When this is called, we should always have a subscription image! + */ + internal fun setImage() { + var triggered = false + while (subscription.hasNoImages()) { + triggered = true + Thread.sleep(50) + } - // NOTE: subscriptions (ie: reading from buffers, etc) are not thread safe! Because it is ambiguous HOW EXACTLY they are unsafe, - // we exclusively read from the DirectBuffer on a single thread. - endPoint.processMessage(buffer, offset, length, header, this@Connection) + if (triggered) { + logger.error("Delay while configuring subscription!") } - @Suppress("LeakingThis") - rmi = connectionParameters.endPoint.rmiConnectionSupport.getNewRmiSupport(this) + image = subscription.imageAtIndex(0) } /** - * @return true if the remote public key changed. This can be useful if specific actions are necessary when the key has changed. + * Polls the AERON media driver subscription channel for incoming messages */ - fun hasRemoteKeyChanged(): Boolean { - return remoteKeyChanged + internal fun poll(): Int { + return image.poll(messageHandler, 1) } -// /** -// * This is the per-message sequence number. -// * -// * The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 4 (external counter) + 4 (GCM counter) -// * The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this -// * counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small) -// */ -// fun nextGcmSequence(): Long { -// return aes_gcm_iv.getAndIncrement() -// } -// -// /** -// * @return the AES key. key=32 byte, iv=12 bytes (AES-GCM implementation). -// */ -// fun cryptoKey(): SecretKey { -// TODO() -//// return channelWrapper.cryptoKey() -// } - - /** - * Polls the AERON media driver subscription channel for incoming messages + * Safely sends objects to a destination, if `abortEarly` is true, there are no retries if sending the message fails. + * + * @return true if the message was successfully sent, false otherwise. Exceptions are caught and NOT rethrown! */ - internal fun poll(): Int { - // NOTE: regarding fragment limit size. Repeated calls to '.poll' will reassemble a fragment. - // `.poll(handler, 4)` == `.poll(handler, 2)` + `.poll(handler, 2)` - return subscription.poll(messageHandler, 1) + internal fun send(message: Any, abortEarly: Boolean): Boolean { + if (logger.isTraceEnabled) { + // The handshake sessionId IS NOT globally unique + // don't automatically create the lambda when trace is disabled! Because this uses 'outside' scoped info, it's a new lambda each time! + if (logger.isTraceEnabled) { + logger.trace("[$toString0] send: ${message.javaClass.simpleName} : $message") + } + } + + val success = endPoint.write(message, publication, sendIdleStrategy, this@Connection, maxMessageSize, abortEarly) + + return if (!success && message !is DisconnectMessage) { + // queue up the messages, because we couldn't write them for whatever reason! + // NEVER QUEUE THE DISCONNECT MESSAGE! + bufferedSession.queueMessage(this@Connection, message, abortEarly) + } else { + success + } + } + + private fun sendNoBuffer(message: Any): Boolean { + if (logger.isTraceEnabled) { + // The handshake sessionId IS NOT globally unique + // don't automatically create the lambda when trace is disabled! Because this uses 'outside' scoped info, it's a new lambda each time! + if (logger.isTraceEnabled) { + logger.trace("[$toString0] send: ${message.javaClass.simpleName} : $message") + } + } + + return endPoint.write(message, publication, sendIdleStrategy, this@Connection, maxMessageSize, false) } /** @@ -194,11 +267,20 @@ open class Connection(connectionParameters: ConnectionParams<*>) { * @return true if the message was successfully sent, false otherwise. Exceptions are caught and NOT rethrown! */ fun send(message: Any): Boolean { - messagesInProgress.getAndIncrement() - val success = endPoint.send(message, publication, this) - messagesInProgress.getAndDecrement() + return send(message, false) + } + - return success + /** + * Safely sends objects to a destination, where the callback is notified once the remote endpoint has received the message. + * + * This is to guarantee happens-before, and using this will depend upon APP+NETWORK latency, and is (by design) not as performant as + * sending a regular message! + * + * @return true if the message was successfully sent, false otherwise. Exceptions are caught and NOT rethrown! + */ + fun send(message: Any, onSuccessCallback: Connection.() -> Unit): Boolean { + return sendSync(message, onSuccessCallback) } /** @@ -206,17 +288,19 @@ open class Connection(connectionParameters: ConnectionParams<*>) { * * @return true if the message was successfully sent by aeron */ - suspend fun ping(pingTimeoutSeconds: Int = PingManager.DEFAULT_TIMEOUT_SECONDS, function: suspend Ping.() -> Unit): Boolean { - return endPoint.ping(this, pingTimeoutSeconds, function) + fun ping(function: Ping.() -> Unit = {}): Boolean { + return sendPing(function) } /** - * A message in progress means that we have requested to to send an object over the network, but it hasn't finished sending over the network + * This is the per-message sequence number. * - * @return the number of messages in progress for this connection. + * The IV for AES-GCM must be 12 bytes, since it's 4 (salt) + 4 (external counter) + 4 (GCM counter) + * The 12 bytes IV is created during connection registration, and during the AES-GCM crypto, we override the last 8 with this + * counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small) */ - fun messagesInProgress(): Int { - return messagesInProgress.value + internal fun nextGcmSequence(): Int { + return aes_gcm_iv.getAndIncrement() } /** @@ -229,10 +313,10 @@ open class Connection(connectionParameters: ConnectionParams<*>) { * (via connection.addListener), meaning that ONLY that listener attached to * the connection is notified on that event (ie, admin type listeners) */ - suspend fun onDisconnect(function: suspend Connection.() -> Unit) { + fun onDisconnect(function: Connection.() -> Unit) { // make sure we atomically create the listener manager, if necessary listenerManager.getAndUpdate { origManager -> - origManager ?: ListenerManager(logger) + origManager ?: ListenerManager(logger, endPoint.eventDispatch) } listenerManager.value!!.onDisconnect(function) @@ -241,10 +325,10 @@ open class Connection(connectionParameters: ConnectionParams<*>) { /** * Adds a function that will be called only for this connection, when a client/server receives a message */ - suspend fun onMessage(function: suspend Connection.(MESSAGE) -> Unit) { + fun onMessage(function: Connection.(MESSAGE) -> Unit) { // make sure we atomically create the listener manager, if necessary listenerManager.getAndUpdate { origManager -> - origManager ?: ListenerManager(logger) + origManager ?: ListenerManager(logger, endPoint.eventDispatch) } listenerManager.value!!.onMessage(function) @@ -255,43 +339,67 @@ open class Connection(connectionParameters: ConnectionParams<*>) { * * This is ALWAYS called on a new dispatch */ - internal suspend fun notifyOnMessage(message: Any): Boolean { + internal fun notifyOnMessage(message: Any): Boolean { return listenerManager.value?.notifyOnMessage(this, message) ?: false } + internal fun sendBufferedMessages() { + if (enableBufferedMessages) { + val bufferedMessage = BufferedMessages() + val numberDrained = bufferedSession.pendingMessagesQueue.drainTo(bufferedMessage.messages) + + if (numberDrained > 0) { + // now send all buffered/pending messages + if (logger.isDebugEnabled) { + logger.debug("Sending buffered messages: ${bufferedSession.pendingMessagesQueue.size}") + } + + sendNoBuffer(bufferedMessage) + } + } + } + + /** + * @return true if this connection has had close() called + */ + fun isClosed(): Boolean { + return isClosed.value + } + + /** + * Is this a "dirty" disconnect, meaning that it has timed out, but not been explicitly closed + */ + internal fun isDirtyClose(): Boolean { + return !closeRequested && !isClosed() && isClosedWithTimeout() + } + + /** + * Is this connection considered still safe for polling (or rather, has it been closed in an unusual way?) + */ + internal fun canPoll(): Boolean { + return !closeRequested && !isClosed() && !isClosedWithTimeout() + } + /** * We must account for network blips. The blips will be recovered by aeron, but we want to make sure that we are actually * disconnected for a set period of time before we start the close process for a connection * * @return `true` if this connection has been closed via aeron */ - fun isClosedViaAeron(): Boolean { + internal fun isClosedWithTimeout(): Boolean { // we ONLY want to actually, legit check, 1 time every XXX ms. val now = System.nanoTime() - if (now - connectionLastCheckTimeNanos < connectionCheckIntervalNanos) { - // we haven't waited long enough for another check. always return false (true means we are closed) - return false - } - connectionLastCheckTimeNanos = now - // as long as we are connected, we reset the state, so that if there is a network blip, we want to make sure that it is - // a network blip for a while, instead of just once or twice. (which can happen) + // a network blip for a while, instead of just once or twice. (which WILL happen) if (subscription.isConnected && publication.isConnected) { // reset connection timeout - connectionTimeoutTimeNanos = 0L + connectionTimeoutTimeNanos = now // we are still connected (true means we are closed) return false } - // - // aeron is not connected - // - - if (connectionTimeoutTimeNanos == 0L) { - connectionTimeoutTimeNanos = now - } // make sure that our "isConnected" state lasts LONGER than the expiry timeout! @@ -300,92 +408,154 @@ open class Connection(connectionParameters: ConnectionParams<*>) { return now - connectionTimeoutTimeNanos >= connectionExpirationTimoutNanos } + /** * Closes the connection, and removes all connection specific listeners */ fun close() { - close(enableRemove = true) + close(sendDisconnectMessage = true, + closeEverything = true) } /** * Closes the connection, and removes all connection specific listeners */ - internal fun close(enableRemove: Boolean) { + internal fun close(sendDisconnectMessage: Boolean, closeEverything: Boolean) { // there are 2 ways to call close. // MANUALLY // When a connection is disconnected via a timeout/expire. + // the compareAndSet is used to make sure that if we call close() MANUALLY, (and later) when the auto-cleanup/disconnect is called -- it doesn't // try to do it again. + closeRequested = true + + // make sure that EVERYTHING before "close()" runs before we do. + // If there are multiple clients/servers sharing the same NetworkPoller -- then they will wait on each other! + val close = endPoint.eventDispatch.CLOSE + if (!close.isDispatch()) { + close.launch { + close(sendDisconnectMessage = sendDisconnectMessage, closeEverything = closeEverything) + } + return + } + + closeImmediately(sendDisconnectMessage = sendDisconnectMessage, closeEverything = closeEverything) + } + + // connection.close() -> this + // endpoint.close() -> connection.close() -> this + internal fun closeImmediately(sendDisconnectMessage: Boolean, closeEverything: Boolean) { // the server 'handshake' connection info is cleaned up with the disconnect via timeout/expire. - if (isClosed.compareAndSet(expect = false, update = true)) { - val aeronLogInfo = "${id}/${streamId}" - logger.debug {"[$aeronLogInfo] connection closing"} + if (!isClosed.compareAndSet(expect = false, update = true)) { + logger.debug("[$toString0] connection ignoring close request.") + return + } - subscription.close() + if (logger.isDebugEnabled) { + logger.debug("[$toString0] connection closing. sendDisconnectMessage=$sendDisconnectMessage, closeEverything=$closeEverything") + } - // send out a "close" message. MAYBE it gets to the remote endpoint, maybe not. If it DOES, then the remote endpoint starts - // the close process faster. - try { - endPoint.send(CloseMessage(), publication, this) - } catch (ignored: Exception) { + // make sure to save off the RMI objects for session management + if (!closeEverything) { + when (endPoint) { + is Server -> endPoint.bufferedManager.onDisconnect(this) + is Client -> endPoint.bufferedManager!!.onDisconnect(this) + else -> throw RuntimeException("Unable to determine type, aborting!") } + } + + if (!closeEverything) { + when (endPoint) { + is Server -> endPoint.bufferedManager.onDisconnect(this) + is Client -> endPoint.bufferedManager!!.onDisconnect(this) + else -> throw RuntimeException("Unable to determine type, aborting!") + } + } + // on close, we want to make sure this file is DELETED! + try { + // we might not be able to close this connection!! + endPoint.aeronDriver.close(subscription, toString0) + } + catch (e: Exception) { + endPoint.listenerManager.notifyError(e) + } + // notify the remote endPoint that we are closing + // we send this AFTER we close our subscription (so that no more messages will be received, when the remote end ping-pong's this message back) + if (sendDisconnectMessage) { + if (publication.isConnected) { + if (logger.isDebugEnabled) { + logger.debug("Sending disconnect message to ${endPoint.otherTypeName}") + } - val timoutInNanos = TimeUnit.SECONDS.toNanos(endPoint.config.connectionCloseTimeoutInSeconds.toLong()) - var closeTimeoutTime = System.nanoTime() + // sometimes the remote end has already disconnected, THERE WILL BE ERRORS if this happens (but they are ok) + if (closeEverything) { + send(DisconnectMessage.CLOSE_EVERYTHING, true) + } else { + send(DisconnectMessage.CLOSE_SIMPLE, true) + } - // we do not want to close until AFTER all publications have been sent. Calling this WITHOUT waiting will instantly stop everything - // we want a timeout-check, otherwise this will run forever - while (messagesInProgress.value != 0 && System.nanoTime() - closeTimeoutTime < timoutInNanos) { - sleep(50) + // wait for .5 seconds to (help) make sure that the messages are sent before shutdown! This is not guaranteed! + if (logger.isDebugEnabled) { + logger.debug("Waiting for disconnect message to send") + } + Thread.sleep(500L) + } else { + if (logger.isDebugEnabled) { + logger.debug("Publication is not connected with ${endPoint.otherTypeName}, not sending disconnect message.") + } } + } - // on close, we want to make sure this file is DELETED! - val logFile = endPoint.aeronDriver.getMediaDriverPublicationFile(publication.registrationId()) - publication.close() + // on close, we want to make sure this file is DELETED! + try { + // we might not be able to close this connection. + endPoint.aeronDriver.close(publication, toString0) + } + catch (e: Exception) { + endPoint.listenerManager.notifyError(e) + } + // NOTE: any waiting RMI messages that are in-flight will terminate when they time-out (and then do nothing) + // if there are errors within the driver, we do not want to notify disconnect, as we will automatically reconnect. + endPoint.listenerManager.notifyDisconnect(this) - closeTimeoutTime = System.nanoTime() - while (logFile.exists() && System.nanoTime() - closeTimeoutTime < timoutInNanos) { - if (logFile.delete()) { - break - } - sleep(100) - } + endPoint.removeConnection(this) - if (logFile.exists()) { - logger.error("[$aeronLogInfo] Unable to delete aeron publication log on close: $logFile") - } - if (enableRemove) { - endPoint.removeConnection(this) + val connection = this + if (endPoint.isServer()) { + // clean up the resources associated with this connection when it's closed + if (logger.isDebugEnabled) { + logger.debug("[${connection}] freeing resources") } + sessionIdAllocator.free(info.sessionIdPub) + sessionIdAllocator.free(info.sessionIdSub) - // NOTE: notifyDisconnect() is called inside closeAction()!! + streamIdAllocator.free(info.streamIdPub) + streamIdAllocator.free(info.streamIdSub) - // This is set by the client/server so if there is a "connect()" call in the the disconnect callback, we can have proper - // lock-stop ordering for how disconnect and connect work with each-other - runBlocking { - closeAction() + if (remoteAddress != null) { + // unique for UDP endpoints + (endPoint as Server).handshake.connectionsPerIpCounts.decrementSlow(remoteAddress) } - logger.debug {"[$aeronLogInfo] connection closed"} + } + + if (logger.isDebugEnabled) { + logger.debug("[$toString0] connection closed") } } - // called in postCloseAction(), so we don't expose our internal listenerManager - internal suspend fun doNotifyDisconnect() { + + // called in a ListenerManager.notifyDisconnect(), so we don't expose our internal listenerManager + internal fun notifyDisconnect() { val connectionSpecificListenerManager = listenerManager.value - connectionSpecificListenerManager?.notifyDisconnect(this@Connection) + connectionSpecificListenerManager?.directNotifyDisconnect(this@Connection) } - // - // - // Generic object methods - // - // override fun toString(): String { return toString0 } @@ -409,17 +579,93 @@ open class Connection(connectionParameters: ConnectionParams<*>) { return id == other1.id } - // cleans up the connection information - internal fun cleanup(connectionsPerIpCounts: ConnectionCounts, sessionIdAllocator: RandomId65kAllocator, streamIdAllocator: RandomId65kAllocator) { - sessionIdAllocator.free(id) + internal fun receiveSendSync(sendSync: SendSync) { + if (sendSync.message != null) { + // this is on the "remote end". + sendSync.message = null - if (isIpc) { - streamIdAllocator.free(publicationPort) - streamIdAllocator.free(subscriptionPort) + if (!send(sendSync)) { + logger.error("Error returning send-sync: $sendSync") + } } else { - // unique for UDP endpoints - connectionsPerIpCounts.decrementSlow(remoteAddress!!) - streamIdAllocator.free(streamId) + // this is on the "local end" when the response comes back + val responseId = sendSync.id + + // process the ping message so that our ping callback does something + + // this will be null if the ping took longer than XXX seconds and was cancelled + val result = EndPoint.responseManager.removeWaiterCallback Unit>(responseId, logger) + if (result != null) { + result(this) + } else { + logger.error("Unable to receive send-sync, there was no waiting response for $sendSync ($responseId)") + } } } + + + /** + * Safely sends objects to a destination, the callback is notified once the remote endpoint has received the message. + * + * This is to guarantee happens-before, and using this will depend upon APP+NETWORK latency, and is (by design) not as performant as + * sending a regular message! + * + * @return true if the message was successfully sent, false otherwise. Exceptions are caught and NOT rethrown! + */ + private fun sendSync(message: Any, onSuccessCallback: Connection.() -> Unit): Boolean { + val id = EndPoint.responseManager.prepWithCallback(logger, onSuccessCallback) + + val sendSync = SendSync() + sendSync.message = message + sendSync.id = id + + // if there is no sync response EVER, it means that the connection is in a critically BAD state! + // eventually, all the ping/sync replies (or, in our case, the replies that have timed out) will + // become recycled. + // Is it a memory-leak? No, because the memory will **EVENTUALLY** get freed. + + return send(sendSync, false) + } + + + internal fun receivePing(ping: Ping) { + if (ping.pongTime == 0L) { + // this is on the "remote end". + ping.pongTime = System.currentTimeMillis() + + if (!send(ping)) { + logger.error("Error returning ping: $ping") + } + } else { + // this is on the "local end" when the response comes back + ping.finishedTime = System.currentTimeMillis() + + val responseId = ping.packedId + + // process the ping message so that our ping callback does something + + // this will be null if the ping took longer than XXX seconds and was cancelled + val result = EndPoint.responseManager.removeWaiterCallback Unit>(responseId, logger) + if (result != null) { + result(ping) + } else { + logger.error("Unable to receive ping, there was no waiting response for $ping ($responseId)") + } + } + } + + private fun sendPing(function: Ping.() -> Unit): Boolean { + val id = EndPoint.responseManager.prepWithCallback(logger, function) + + val ping = Ping() + ping.packedId = id + ping.pingTime = System.currentTimeMillis() + + // if there is no ping response EVER, it means that the connection is in a critically BAD state! + // eventually, all the ping replies (or, in our case, the RMI replies that have timed out) will + // become recycled. + // Is it a memory-leak? No, because the memory will **EVENTUALLY** get freed. + + return send(ping) + } } diff --git a/src/dorkbox/network/connection/ConnectionManager.kt b/src/dorkbox/network/connection/ConnectionManager.kt deleted file mode 100644 index 669c1097..00000000 --- a/src/dorkbox/network/connection/ConnectionManager.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2020 dorkbox, llc - * - * 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 dorkbox.network.connection - -import dorkbox.collections.ConcurrentEntry -import dorkbox.collections.ConcurrentIterator -import dorkbox.collections.ConcurrentIterator.headREF - -// .equals() compares the identity on purpose,this because we cannot create two separate objects that are somehow equal to each other. -@Suppress("UNCHECKED_CAST") -internal open class ConnectionManager() { - - private val connections = ConcurrentIterator() - - - /** - * Invoked when aeron successfully connects to a remote address. - * - * @param connection the connection to add - */ - fun add(connection: CONNECTION) { - connections.add(connection) - } - - /** - * Removes a custom connection to the server. - * - * This should only be used in situations where there can be DIFFERENT types of connections (such as a 'web-based' connection) and - * you want *this* server instance to manage listeners + message dispatch - * - * @param connection the connection to remove - */ - fun remove(connection: CONNECTION) { - connections.remove(connection) - } - - /** - * Performs an action on each connection in the list. - */ - inline fun forEach(function: (connection: CONNECTION) -> Unit) { - // access a snapshot (single-writer-principle) - val head = headREF.get(connections) as ConcurrentEntry? - var current: ConcurrentEntry? = head - - var connection: CONNECTION - while (current != null) { - // Concurrent iteration... - connection = current.value - current = current.next() - - function(connection) - } - } - - fun connectionCount(): Int { - return connections.size() - } - - /** - * Removes all connections. Does not call close or anything else on them - */ - fun clear() { - connections.clear() - } -} diff --git a/src/dorkbox/network/connection/ConnectionParams.kt b/src/dorkbox/network/connection/ConnectionParams.kt index 6a9a799a..4c40092e 100644 --- a/src/dorkbox/network/connection/ConnectionParams.kt +++ b/src/dorkbox/network/connection/ConnectionParams.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,14 @@ */ package dorkbox.network.connection -import dorkbox.network.aeron.mediaDriver.MediaDriverConnectInfo +import dorkbox.network.handshake.PubSub +import javax.crypto.spec.SecretKeySpec data class ConnectionParams( + val publicKey: ByteArray, val endPoint: EndPoint, - val connectionInfo: MediaDriverConnectInfo, - val publicKeyValidation: PublicKeyValidationState + val connectionInfo: PubSub, + val publicKeyValidation: PublicKeyValidationState, + val enableBufferedMessages: Boolean, + val cryptoKey: SecretKeySpec ) diff --git a/src/dorkbox/network/connection/CryptoManagement.kt b/src/dorkbox/network/connection/CryptoManagement.kt index 648cfe1f..0db003f5 100644 --- a/src/dorkbox/network/connection/CryptoManagement.kt +++ b/src/dorkbox/network/connection/CryptoManagement.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package dorkbox.network.connection import dorkbox.bytes.Hash -import dorkbox.bytes.toHexString +import dorkbox.hex.toHexString import dorkbox.network.handshake.ClientConnectionInfo import dorkbox.network.serialization.AeronInput import dorkbox.network.serialization.AeronOutput import dorkbox.network.serialization.SettingsStore import dorkbox.util.entropy.Entropy -import mu.KLogger +import org.slf4j.Logger import java.math.BigInteger import java.net.InetAddress import java.security.KeyFactory @@ -42,33 +42,39 @@ import javax.crypto.spec.SecretKeySpec /** * Management for all the crypto stuff used */ -internal class CryptoManagement(val logger: KLogger, +internal class CryptoManagement(val logger: Logger, private val settingsStore: SettingsStore, type: Class<*>, private val enableRemoteSignatureValidation: Boolean) { + companion object { + private val X25519 = "X25519" + const val curve25519 = "curve25519" + + const val GCM_IV_LENGTH_BYTES = 12 // 12 bytes for a 96-bit IV + const val GCM_TAG_LENGTH_BITS = 128 + const val AES_ALGORITHM = "AES/GCM/NoPadding" + + val NOCRYPT = SecretKeySpec(ByteArray(1), "NOCRYPT") + val secureRandom = SecureRandom() + } + - private val X25519 = "X25519" private val X25519KeySpec = NamedParameterSpec(X25519) private val keyFactory = KeyFactory.getInstance(X25519) // key size is 32 bytes (256 bits) private val keyAgreement = KeyAgreement.getInstance("XDH") - private val aesCipher = Cipher.getInstance("AES/GCM/NoPadding") + private val aesCipher = Cipher.getInstance(AES_ALGORITHM) + - companion object { - const val curve25519 = "curve25519" - const val GCM_IV_LENGTH_BYTES = 12 - const val GCM_TAG_LENGTH_BITS = 128 - } val privateKey: XECPrivateKey val publicKey: XECPublicKey + // These are both 32 bytes long (256 bits) val privateKeyBytes: ByteArray val publicKeyBytes: ByteArray - val secureRandom = SecureRandom(settingsStore.getSalt()) - private val iv = ByteArray(GCM_IV_LENGTH_BYTES) val cryptOutput = AeronOutput() val cryptInput = AeronInput() @@ -78,12 +84,17 @@ internal class CryptoManagement(val logger: KLogger, logger.warn("WARNING: Disabling remote key validation is a security risk!!") } + secureRandom.setSeed(settingsStore.salt) + // initialize the private/public keys used for negotiating ECC handshakes // these are ONLY used for IP connections. LOCAL connections do not need a handshake! - var privateKeyBytes = settingsStore.getPrivateKey() - var publicKeyBytes = settingsStore.getPublicKey() + val privateKeyBytes: ByteArray + val publicKeyBytes: ByteArray - if (privateKeyBytes == null || publicKeyBytes == null) { + if (settingsStore.validKeys()) { + privateKeyBytes = settingsStore.privateKey + publicKeyBytes = settingsStore.publicKey + } else { try { // seed our RNG based off of this and create our ECC keys val seedBytes = Entropy["There are no ECC keys for the ${type.simpleName} yet"] @@ -98,8 +109,8 @@ internal class CryptoManagement(val logger: KLogger, privateKeyBytes = xdhPrivate.scalar // save to properties file - settingsStore.savePrivateKey(privateKeyBytes) - settingsStore.savePublicKey(publicKeyBytes) + settingsStore.privateKey = privateKeyBytes + settingsStore.publicKey = publicKeyBytes } catch (e: Exception) { val message = "Unable to initialize/generate ECC keys. FORCED SHUTDOWN." logger.error(message, e) @@ -107,14 +118,12 @@ internal class CryptoManagement(val logger: KLogger, } } - publicKeyBytes!! - logger.info("ECC public key: ${publicKeyBytes.toHexString()}") this.publicKey = keyFactory.generatePublic(XECPublicKeySpec(X25519KeySpec, BigInteger(publicKeyBytes))) as XECPublicKey this.privateKey = keyFactory.generatePrivate(XECPrivateKeySpec(X25519KeySpec, privateKeyBytes)) as XECPrivateKey - this.privateKeyBytes = privateKeyBytes!! + this.privateKeyBytes = privateKeyBytes this.publicKeyBytes = publicKeyBytes } @@ -167,10 +176,77 @@ internal class CryptoManagement(val logger: KLogger, return PublicKeyValidationState.VALID } + private fun makeInfo(serverPublicKeyBytes: ByteArray, secretKey: SecretKeySpec): ClientConnectionInfo { + val sessionIdPub = cryptInput.readInt() + val sessionIdSub = cryptInput.readInt() + val streamIdPub = cryptInput.readInt() + val streamIdSub = cryptInput.readInt() + val regDetailsSize = cryptInput.readInt() + val sessionTimeout = cryptInput.readLong() + val bufferedMessages = cryptInput.readBoolean() + val regDetails = cryptInput.readBytes(regDetailsSize) + + // now save data off + return ClientConnectionInfo( + sessionIdPub = sessionIdPub, + sessionIdSub = sessionIdSub, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + publicKey = serverPublicKeyBytes, + sessionTimeout = sessionTimeout, + bufferedMessages = bufferedMessages, + kryoRegistrationDetails = regDetails, + secretKey = secretKey) + } + + // NOTE: ALWAYS CALLED ON THE SAME THREAD! (from the server, mutually exclusive calls to decrypt) + fun nocrypt( + sessionIdPub: Int, + sessionIdSub: Int, + streamIdPub: Int, + streamIdSub: Int, + sessionTimeout: Long, + bufferedMessages: Boolean, + kryoRegDetails: ByteArray + ): ByteArray { + + return try { + // now create the byte array that holds all our data + cryptOutput.reset() + cryptOutput.writeInt(sessionIdPub) + cryptOutput.writeInt(sessionIdSub) + cryptOutput.writeInt(streamIdPub) + cryptOutput.writeInt(streamIdSub) + cryptOutput.writeInt(kryoRegDetails.size) + cryptOutput.writeLong(sessionTimeout) + cryptOutput.writeBoolean(bufferedMessages) + cryptOutput.writeBytes(kryoRegDetails) + + cryptOutput.toBytes() + } catch (e: Exception) { + logger.error("Error during AES encrypt", e) + ByteArray(0) + } + } + + // NOTE: ALWAYS CALLED ON THE SAME THREAD! (from the client, mutually exclusive calls to encrypt) + fun nocrypt(registrationData: ByteArray, serverPublicKeyBytes: ByteArray): ClientConnectionInfo? { + return try { + // The message was intended for this client. Try to parse it as one of the available message types. + // this message is NOT-ENCRYPTED! + cryptInput.buffer = registrationData + + makeInfo(serverPublicKeyBytes, NOCRYPT) + } catch (e: Exception) { + logger.error("Error during IPC decrypt!", e) + null + } + } + /** * Generate the AES key based on ECDH */ - private fun generateAesKey(remotePublicKeyBytes: ByteArray, bytesA: ByteArray, bytesB: ByteArray): SecretKeySpec { + internal fun generateAesKey(remotePublicKeyBytes: ByteArray, bytesA: ByteArray, bytesB: ByteArray): SecretKeySpec { val clientPublicKey = keyFactory.generatePublic(XECPublicKeySpec(X25519KeySpec, BigInteger(remotePublicKeyBytes))) keyAgreement.init(privateKey) keyAgreement.doPhase(clientPublicKey, true) @@ -187,25 +263,32 @@ internal class CryptoManagement(val logger: KLogger, } // NOTE: ALWAYS CALLED ON THE SAME THREAD! (from the server, mutually exclusive calls to decrypt) - fun encrypt(clientPublicKeyBytes: ByteArray, - subscriptionPort: Int, - connectionSessionId: Int, - connectionStreamId: Int, - kryoRegDetails: ByteArray): ByteArray { + fun encrypt( + cryptoSecretKey: SecretKeySpec, + sessionIdPub: Int, + sessionIdSub: Int, + streamIdPub: Int, + streamIdSub: Int, + sessionTimeout: Long, + bufferedMessages: Boolean, + kryoRegDetails: ByteArray + ): ByteArray { try { - val secretKeySpec = generateAesKey(clientPublicKeyBytes, clientPublicKeyBytes, publicKeyBytes) secureRandom.nextBytes(iv) val gcmParameterSpec = GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv) - aesCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec) + aesCipher.init(Cipher.ENCRYPT_MODE, cryptoSecretKey, gcmParameterSpec) // now create the byte array that holds all our data cryptOutput.reset() - cryptOutput.writeInt(connectionSessionId) - cryptOutput.writeInt(connectionStreamId) - cryptOutput.writeInt(subscriptionPort) + cryptOutput.writeInt(sessionIdPub) + cryptOutput.writeInt(sessionIdSub) + cryptOutput.writeInt(streamIdPub) + cryptOutput.writeInt(streamIdSub) cryptOutput.writeInt(kryoRegDetails.size) + cryptOutput.writeLong(sessionTimeout) + cryptOutput.writeBoolean(bufferedMessages) cryptOutput.writeBytes(kryoRegDetails) return iv + aesCipher.doFinal(cryptOutput.toBytes()) @@ -217,7 +300,7 @@ internal class CryptoManagement(val logger: KLogger, // NOTE: ALWAYS CALLED ON THE SAME THREAD! (from the client, mutually exclusive calls to encrypt) fun decrypt(registrationData: ByteArray, serverPublicKeyBytes: ByteArray): ClientConnectionInfo? { - try { + return try { val secretKeySpec = generateAesKey(serverPublicKeyBytes, publicKeyBytes, serverPublicKeyBytes) // now decrypt the data @@ -226,21 +309,11 @@ internal class CryptoManagement(val logger: KLogger, cryptInput.buffer = aesCipher.doFinal(registrationData, GCM_IV_LENGTH_BYTES, registrationData.size - GCM_IV_LENGTH_BYTES) - val sessionId = cryptInput.readInt() - val streamId = cryptInput.readInt() - val subscriptionPort = cryptInput.readInt() - val regDetailsSize = cryptInput.readInt() - val regDetails = cryptInput.readBytes(regDetailsSize) - - // now read data off - return ClientConnectionInfo(sessionId = sessionId, - streamId = streamId, - port = subscriptionPort, - publicKey = serverPublicKeyBytes, - kryoRegistrationDetails = regDetails) + makeInfo(serverPublicKeyBytes, secretKeySpec) + } catch (e: Exception) { logger.error("Error during AES decrypt!", e) - return null + null } } diff --git a/src/dorkbox/network/rmi/messages/ConnectionObjectDeleteResponse.kt b/src/dorkbox/network/connection/DisconnectMessage.kt similarity index 66% rename from src/dorkbox/network/rmi/messages/ConnectionObjectDeleteResponse.kt rename to src/dorkbox/network/connection/DisconnectMessage.kt index cac176c2..07674ad9 100644 --- a/src/dorkbox/network/rmi/messages/ConnectionObjectDeleteResponse.kt +++ b/src/dorkbox/network/connection/DisconnectMessage.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.network.rmi.messages -/** - * @param rmiId which rmi object was deleted - */ -data class ConnectionObjectDeleteResponse(val rmiId: Int) : RmiMessage { - override fun toString(): String { - return "ConnectionObjectDeleteResponse(id: $rmiId)" +package dorkbox.network.connection + +class DisconnectMessage(val closeEverything: Boolean) { + companion object { + val CLOSE_SIMPLE = DisconnectMessage(false) + val CLOSE_EVERYTHING = DisconnectMessage(true) } } diff --git a/src/dorkbox/network/connection/EndPoint.kt b/src/dorkbox/network/connection/EndPoint.kt index 643925c4..26034350 100644 --- a/src/dorkbox/network/connection/EndPoint.kt +++ b/src/dorkbox/network/connection/EndPoint.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2024 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,57 +15,55 @@ */ package dorkbox.network.connection +import dorkbox.collections.ConcurrentIterator +import dorkbox.netUtil.IP import dorkbox.network.Client import dorkbox.network.Configuration import dorkbox.network.Server import dorkbox.network.ServerConfiguration import dorkbox.network.aeron.AeronDriver import dorkbox.network.aeron.BacklogStat +import dorkbox.network.aeron.EventPoller +import dorkbox.network.connection.buffer.BufferedMessages import dorkbox.network.connection.streaming.StreamingControl import dorkbox.network.connection.streaming.StreamingData import dorkbox.network.connection.streaming.StreamingManager -import dorkbox.network.exceptions.ClientException -import dorkbox.network.exceptions.ServerException -import dorkbox.network.handshake.HandshakeMessage -import dorkbox.network.ipFilter.IpFilterRule +import dorkbox.network.exceptions.* +import dorkbox.network.handshake.Handshaker import dorkbox.network.ping.Ping -import dorkbox.network.ping.PingManager import dorkbox.network.rmi.ResponseManager import dorkbox.network.rmi.RmiManagerConnections import dorkbox.network.rmi.RmiManagerGlobal import dorkbox.network.rmi.messages.MethodResponse import dorkbox.network.rmi.messages.RmiMessage -import dorkbox.network.serialization.KryoExtra +import dorkbox.network.serialization.KryoReader +import dorkbox.network.serialization.KryoWriter import dorkbox.network.serialization.Serialization import dorkbox.network.serialization.SettingsStore +import dorkbox.objectPool.BoundedPoolObject +import dorkbox.objectPool.ObjectPool +import dorkbox.objectPool.Pool +import dorkbox.os.OS import io.aeron.Publication +import io.aeron.driver.ThreadingMode import io.aeron.logbuffer.Header import kotlinx.atomicfu.atomic -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.plus -import kotlinx.coroutines.runBlocking -import mu.KLogger -import mu.KotlinLogging +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.trySendBlocking import org.agrona.DirectBuffer -import org.agrona.MutableDirectBuffer import org.agrona.concurrent.IdleStrategy +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.util.concurrent.* -fun CoroutineScope.eventLoop(block: suspend CoroutineScope.() -> Unit): Job { - // UNDISPATCHED means that this coroutine will start as an event loop, instead of concurrently in a different thread - // we want this behavior to prevent "stack overflow" in case there are nested calls - return launch(start = CoroutineStart.UNDISPATCHED, block = block) -} + // If TCP and UDP both fill the pipe, THERE WILL BE FRAGMENTATION and dropped UDP packets! // it results in severe UDP packet loss and contention. // // http://www.isoc.org/INET97/proceedings/F3/F3_1.HTM -// also, a google search on just "INET97/proceedings/F3/F3_1.HTM" turns up interesting problems. +// also, a Google search on just "INET97/proceedings/F3/F3_1.HTM" turns up interesting problems. // Usually it's with ISPs. /** * represents the base of a client/server end point for interacting with aeron @@ -76,82 +74,98 @@ fun CoroutineScope.eventLoop(block: suspend CoroutineScope.() -> Unit): Job { * * @throws SecurityException if unable to initialize/generate ECC keys */ -abstract class EndPoint -internal constructor(val type: Class<*>, - internal val config: Configuration, - internal val connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION, - loggerName: String) - : AutoCloseable { +abstract class EndPoint private constructor(val type: Class<*>, val config: Configuration, loggerName: String) { protected constructor(config: Configuration, - connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION, loggerName: String) - : this(Client::class.java, config, connectionFunc, loggerName) + : this(Client::class.java, config, loggerName) protected constructor(config: ServerConfiguration, - connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION, loggerName: String) - : this(Server::class.java, config, connectionFunc, loggerName) + : this(Server::class.java, config, loggerName) companion object { - /** - * @return the error code text for the specified number - */ - private fun errorCodeName(result: Long): String { - return when (result) { - // The publication is not connected to a subscriber, this can be an intermittent state as subscribers come and go. - Publication.NOT_CONNECTED -> "Not connected" - - // The offer failed due to back pressure from the subscribers preventing further transmission. - Publication.BACK_PRESSURED -> "Back pressured" + // connections are extremely difficult to diagnose when the connection timeout is short + internal const val DEBUG_CONNECTIONS = false - // The action is an operation such as log rotation which is likely to have succeeded by the next retry attempt. - Publication.ADMIN_ACTION -> "Administrative action" + internal const val IPC_NAME = "IPC" - // The Publication has been closed and should no longer be used. - Publication.CLOSED -> "Publication is closed" + internal val networkEventPoller = EventPoller() + internal val responseManager = ResponseManager() - // If this happens then the publication should be closed and a new one added. To make it less likely to happen then increase the term buffer length. - Publication.MAX_POSITION_EXCEEDED -> "Maximum term position exceeded" - - else -> throw IllegalStateException("Unknown error code: $result") - } - } + internal val lanAddress = IP.lanAddress() } - val logger: KLogger = KotlinLogging.logger(loggerName) + val logger: Logger = LoggerFactory.getLogger(loggerName) + + internal val eventDispatch = EventDispatcher(loggerName) private val handler = CoroutineExceptionHandler { _, exception -> - logger.error(exception) { "Uncaught Coroutine Error!" } + logger.error("Uncaught Coroutine Error: ${exception.stackTraceToString()}") } - internal val actionDispatch = config.dispatch + handler + // this is rather silly, BUT if there are more complex errors WITH the coroutine that occur, a regular try/catch WILL NOT catch it. + // ADDITIONALLY, an error handler is ONLY effective at the first, top-level `launch`. IT WILL NOT WORK ANY OTHER WAY. + private val messageCoroutineScope = CoroutineScope(Dispatchers.IO + handler + SupervisorJob()) + private val messageChannel = Channel>() + private val pairedPool: Pool> + + internal val listenerManager = ListenerManager(logger, eventDispatch) - internal val listenerManager = ListenerManager(logger) - internal val connections = ConnectionManager() + val connections = ConcurrentIterator() - internal val aeronDriver: AeronDriver + @Volatile + internal var aeronDriver: AeronDriver /** - * Returns the serialization wrapper if there is an object type that needs to be added outside of the basic types. + * Returns the serialization wrapper if there is an object type that needs to be added in addition to the basic types. */ val serialization: Serialization - private val handshakeKryo: KryoExtra + /** + * Read and Write can be concurrent (different buffers are used) + * GLOBAL, single threaded only kryo instances. + * + * This WILL RE-CONFIGURED during the client handshake! (it is all the same thread, so object visibility is not a problem) + */ + @Volatile + internal lateinit var readKryo: KryoReader - private val sendIdleStrategy: IdleStrategy - private val pollIdleStrategy: IdleStrategy - private val handshakeSendIdleStrategy: IdleStrategy + internal val handshaker: Handshaker /** * Crypto and signature management */ internal val crypto: CryptoManagement - private val shutdown = atomic(false) + + private val hook: Thread + + + // manage the startup state of the endpoint. True if the endpoint is running + internal val endpointIsRunning = atomic(false) + + // this only prevents multiple shutdowns (in the event this close() is called multiple times) + internal var shutdown = atomic(false) + internal val shutdownInProgress = atomic(false) + + @Volatile + internal var shutdownEventPoller = false @Volatile - private var shutdownLatch = CountDownLatch(1) + private var shutdownLatch = CountDownLatch(0) + + /** + * This is run in lock-step to shutdown/close the client/server event poller. Afterward, connect/bind can be called again + */ + @Volatile + internal var pollerClosedLatch = CountDownLatch(0) + + /** + * This is only notified when endpoint.close() is called where EVERYTHING is to be closed. + */ + @Volatile + internal var closeLatch = CountDownLatch(0) /** * Returns the storage used by this endpoint. This is the backing data structure for key/value pairs, and can be a database, file, etc @@ -160,38 +174,69 @@ internal constructor(val type: Class<*>, */ val storage: SettingsStore - internal val responseManager = ResponseManager(logger, actionDispatch) internal val rmiGlobalSupport = RmiManagerGlobal(logger) internal val rmiConnectionSupport: RmiManagerConnections - private val streamingManager = StreamingManager(logger, actionDispatch) + private val streamingManager = StreamingManager(logger, config) + + /** + * The primary machine port that the server will listen for connections on + */ + @Volatile + var port1: Int = 0 + internal set - internal val pingManager = PingManager() + /** + * The secondary machine port that the server will use to work around NAT firewalls (this is required, and will be different from the primary) + */ + @Volatile + var port2: Int = 0 + internal set init { - require(!config.previouslyUsed) { "${type.simpleName} configuration cannot be reused!" } - config.validate() // this happens more than once! (this is ok) + if (DEBUG_CONNECTIONS) { + logger.error("DEBUG_CONNECTIONS is enabled. This should not happen in release!") + } + + // this happens more than once! (this is ok) + config.validate() // serialization stuff @Suppress("UNCHECKED_CAST") serialization = config.serialization as Serialization - sendIdleStrategy = config.sendIdleStrategy.cloneToNormal() - pollIdleStrategy = config.pollIdleStrategy.cloneToNormal() - handshakeSendIdleStrategy = config.sendIdleStrategy.cloneToNormal() + serialization.finishInit(type, config.networkMtuSize) - handshakeKryo = serialization.initHandshakeKryo() + serialization.fileContentsSerializer.streamingManager = streamingManager + + // we are done with initial configuration, now finish serialization + // the CLIENT will reassign these in the `connect0` method (because it registers what the server says to register) + if (type == Server::class.java) { + readKryo = serialization.newReadKryo() + } - // we have to be able to specify the property store - storage = SettingsStore(config.settingsStore.logger(logger), logger) + // we have to be able to specify the property store + storage = SettingsStore(config.settingsStore, logger) crypto = CryptoManagement(logger, storage, type, config.enableRemoteSignatureValidation) // Only starts the media driver if we are NOT already running! - try { - aeronDriver = AeronDriver(config, type, logger, listenerManager.notifyError) + // NOTE: in the event that we are IPC -- only ONE SERVER can be running IPC at a time for a single driver! + if (type == Server::class.java && config.enableIpc) { + val configuration = config.copy() + if (AeronDriver.isLoaded(configuration, logger)) { + val e = ServerException("Only one server at a time can share a single aeron driver! Make the driver unique or change it's directory: ${configuration.aeronDirectory}") + listenerManager.notifyError(e) + throw e + } + } + + aeronDriver = try { + @Suppress("LeakingThis") + AeronDriver.new(this@EndPoint) } catch (e: Exception) { - logger.error("Error initialize endpoint", e) - throw e + val exception = Exception("Error initializing endpoint", e) + listenerManager.notifyError(exception) + throw exception } rmiConnectionSupport = if (type.javaClass == Server::class.java) { @@ -204,24 +249,139 @@ internal constructor(val type: Class<*>, return@RmiManagerConnections rmiGlobalSupport.getGlobalRemoteObject(connection, objectId, interfaceClass) } } + + handshaker = Handshaker(logger, config, serialization, listenerManager, aeronDriver) { errorMessage, exception -> + return@Handshaker newException(errorMessage, exception) + } + + hook = Thread { + close(closeEverything = true, sendDisconnectMessage = true, releaseWaitingThreads = true) + } + + Runtime.getRuntime().addShutdownHook(hook) + + + + val poolObject = object : BoundedPoolObject>() { + override fun newInstance(): Paired { + return Paired() + } + } + + // The purpose of this, is to lessen the impact of garbage created on the heap. + pairedPool = ObjectPool.nonBlockingBounded(poolObject, 256) + } + + + internal val typeName: String + get() { + return if (type == Server::class.java) { + "server" + } else { + "client" + } + } + + internal val otherTypeName: String + get() { + return if (type == Server::class.java) { + "client" + } else { + "server" + } + } + + internal fun isServer(): Boolean { + return type === Server::class.java + } + + internal fun isClient(): Boolean { + return type === Client::class.java + } + + /** + * Make sure that shutdown latch is properly initialized + * + * The client calls this every time it attempts a connection. + */ + internal fun initializeState() { + // on repeated runs, we have to make sure that we release the original latches so we don't appear to deadlock. + val origCloseLatch = closeLatch + val origShutdownLatch = shutdownLatch + val origPollerLatch = pollerClosedLatch + + // on the first run, we depend on these to be 0 + shutdownLatch = CountDownLatch(1) + closeLatch = CountDownLatch(1) + + // make sure we don't deadlock if we are waiting for the server to close + origCloseLatch.countDown() + origShutdownLatch.countDown() + origPollerLatch.countDown() + + endpointIsRunning.lazySet(true) + shutdown.lazySet(false) + shutdownEventPoller = false + + // there are threading issues if there are client(s) and server's within the same JVM, where we have thread starvation + // this resolves the problem. Additionally, this is tied-to specific a specific endpoint instance + networkEventPoller.configure(logger, config, this) + + + + // how to select the number of threads that will fetch/use data off the network stack. + // The default is a minimum of 1, but maximum of 4. + // Account for + // - Aeron Threads (3, usually - defined in config) + // - Leave 2 threads for "the box" + + val aeronThreads = when (config.threadingMode) { + ThreadingMode.SHARED -> 1 + ThreadingMode.SHARED_NETWORK -> 2 + ThreadingMode.DEDICATED -> 3 + else -> 3 + } + + + // create a new one when the endpoint starts up, because we close it when the endpoint shuts down or when the client retries + // this leaves 2 for the box and XX for aeron + val messageProcessThreads = (OS.optimumNumberOfThreads - aeronThreads).coerceAtLeast(1).coerceAtMost(4) + + // create a new one when the endpoint starts up, because we close it when the endpoint shuts down or when the client retries + repeat(messageProcessThreads) { + messageCoroutineScope.launch { + // this is only true while the endpoint is running. + while (endpointIsRunning.value) { + val paired = messageChannel.receive() + val connection = paired.connection + val message = paired.message + pairedPool.put(paired) + + processMessageFromChannel(connection, message) + } + } + } } /** * Only starts the media driver if we are NOT already running! * + * If we were previously closed, we will start a new again. This is concurrent safe! + * * @throws Exception if there is a problem starting the media driver */ fun startDriver() { + // recreate the driver if we have previously closed. If we have never run, this does nothing + aeronDriver = aeronDriver.newIfClosed() aeronDriver.start() - shutdown.value = false } /** * Stops the network driver. * * @param forceTerminate if true, then there is no caution when restarting the Aeron driver, and any other process on the machine using - * the same driver will probably crash (unless they have been appropriately stopped). If false (the default), then the Aeron driver is - * only stopped if it is safe to do so + * the same driver will probably crash (unless they have been appropriately stopped). + * If false, then the Aeron driver is only stopped if it is safe to do so */ fun stopDriver(forceTerminate: Boolean = false) { if (forceTerminate) { @@ -231,6 +391,14 @@ internal constructor(val type: Class<*>, } } + /** + * This is called whenever a new connection is made. By overriding this, it is possible to customize the Connection type. + */ + open fun newConnection(connectionParameters: ConnectionParams): CONNECTION { + @Suppress("UNCHECKED_CAST") + return Connection(connectionParameters) as CONNECTION + } + abstract fun newException(message: String, cause: Throwable? = null): Throwable // used internally to remove a connection. Will also remove all proxy objects @@ -266,63 +434,26 @@ internal constructor(val type: Class<*>, connections.remove(connection) } - /** - * Adds an IP+subnet rule that defines if that IP+subnet is allowed/denied connectivity to this server. - * - * By default, if there are no filter rules, then all connections are allowed to connect - * If there are filter rules - then ONLY connections for the filter that returns true are allowed to connect (all else are denied) - * - * This function will be called for **only** network clients (IPC client are excluded) - */ - fun filter(ipFilterRule: IpFilterRule) { - actionDispatch.launch { - listenerManager.filter(ipFilterRule) - } - } - - /** - * Adds a function that will be called BEFORE a client/server "connects" with each other, and used to determine if a connection - * should be allowed - * - * By default, if there are no filter rules, then all connections are allowed to connect - * If there are filter rules - then ONLY connections for the filter that returns true are allowed to connect (all else are denied) - * - * It is the responsibility of the custom filter to write the error, if there is one - * - * If the function returns TRUE, then the connection will continue to connect. - * If the function returns FALSE, then the other end of the connection will - * receive a connection error - * - * This function will be called for **only** network clients (IPC client are excluded) - */ - fun filter(function: CONNECTION.() -> Boolean) { - actionDispatch.launch { - listenerManager.filter(function) - } - } - /** * Adds a function that will be called when a client/server connection is FIRST initialized, but before it's * connected to the remote endpoint. * * NOTE: This callback is executed IN-LINE with network IO, so one must be very careful about what is executed. * + * Things that happen in this event are TIME-CRITICAL, and must happen before anything else. If you block here, you will block network IO + * * For a server, this function will be called for ALL client connections. */ - fun onInit(function: suspend CONNECTION.() -> Unit) { - actionDispatch.launch { - listenerManager.onInit(function) - } + fun onInit(function: CONNECTION.() -> Unit){ + listenerManager.onInit(function) } /** * Adds a function that will be called when a client/server connection first establishes a connection with the remote end. * 'onInit()' callbacks will execute for both the client and server before `onConnect()` will execute will "connects" with each other */ - fun onConnect(function: suspend CONNECTION.() -> Unit) { - actionDispatch.launch { - listenerManager.onConnect(function) - } + fun onConnect(function: CONNECTION.() -> Unit) { + listenerManager.onConnect(function) } /** @@ -330,10 +461,8 @@ internal constructor(val type: Class<*>, * * Do not try to send messages! The connection will already be closed, resulting in an error if you attempt to do so. */ - fun onDisconnect(function: suspend CONNECTION.() -> Unit) { - actionDispatch.launch { - listenerManager.onDisconnect(function) - } + fun onDisconnect(function: CONNECTION.() -> Unit) { + listenerManager.onDisconnect(function) } /** @@ -342,9 +471,7 @@ internal constructor(val type: Class<*>, * The error is also sent to an error log before this method is called. */ fun onError(function: CONNECTION.(Throwable) -> Unit) { - actionDispatch.launch { - listenerManager.onError(function) - } + listenerManager.onError(function) } /** @@ -353,9 +480,7 @@ internal constructor(val type: Class<*>, * The error is also sent to an error log before this method is called. */ fun onErrorGlobal(function: (Throwable) -> Unit) { - actionDispatch.launch { - listenerManager.onError(function) - } + listenerManager.onError(function) } /** @@ -363,402 +488,308 @@ internal constructor(val type: Class<*>, * * This method should not block for long periods as other network activity will not be processed until it returns. */ - fun onMessage(function: suspend CONNECTION.(Message) -> Unit) { - actionDispatch.launch { - listenerManager.onMessage(function) - } + fun onMessage(function: CONNECTION.(Message) -> Unit) { + listenerManager.onMessage(function) } /** - * Sends a "ping" packet to measure **ROUND TRIP** time to the remote connection. + * This is designed to permit modifying/overriding how data is processed on the network. * - * @return true if the message was successfully sent by aeron - */ - internal suspend fun ping(connection: Connection, pingTimeoutMs: Int, function: suspend Ping.() -> Unit): Boolean { - return pingManager.ping(connection, pingTimeoutMs, actionDispatch, responseManager, logger, function) - } - - /** - * NOTE: this **MUST** stay on the same co-routine that calls "send". This cannot be re-dispatched onto a different coroutine! - * CANNOT be called in action dispatch. ALWAYS ON SAME THREAD + * This will split a message if it's too large to send in a single network message. * - * @return true if the message was successfully sent by aeron + * @return true if the message was successfully sent by aeron, false otherwise. Exceptions are caught and NOT rethrown! */ - @Suppress("DuplicatedCode") - internal fun writeHandshakeMessage(publication: Publication, aeronLogInfo: String, message: HandshakeMessage) { - // The handshake sessionId IS NOT globally unique - logger.trace { "[$aeronLogInfo - ${message.connectKey}] send HS: $message" } - - try { - val buffer = handshakeKryo.write(message) - val objectSize = buffer.position() - val internalBuffer = buffer.internalBuffer - - var timeoutInNanos = 0L - var startTime = 0L - - var result: Long - while (true) { - result = publication.offer(internalBuffer, 0, objectSize) - if (result >= 0) { - // success! - return - } - - /** - * Since the publication is not connected, we weren't able to send data to the remote endpoint. - * - * According to Aeron Docs, Pubs and Subs can "come and go", whatever that means. We just want to make sure that we - * don't "loop forever" if a publication is ACTUALLY closed, like on purpose. - */ - if (result == Publication.NOT_CONNECTED) { - if (timeoutInNanos == 0L) { - timeoutInNanos = (aeronDriver.getLingerNs() * 1.2).toLong() // close enough. Just needs to be slightly longer - startTime = System.nanoTime() - } + open fun write( + message: Any, + publication: Publication, + sendIdleStrategy: IdleStrategy, + connection: Connection, + maxMessageSize: Int, + abortEarly: Boolean + ): Boolean { + @Suppress("UNCHECKED_CAST") + connection as CONNECTION - if (System.nanoTime() - startTime < timeoutInNanos) { - // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe - // publication of any state to other threads and not be long running or re-entrant with the client. - // on close, the publication CAN linger (in case a client goes away, and then comes back) - // AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) - - //fixme: this should be the linger timeout, not a retry count! - - // we should retry. - handshakeSendIdleStrategy.idle() - continue - } else if (!publication.isClosed) { - // more critical error sending the message. we shouldn't retry or anything. - // this exception will be a ClientException or a ServerException - val exception = newException( - "[$aeronLogInfo] Error sending message. (Connection in non-connected state longer than linger timeout ${errorCodeName(result)})" + // prep for idle states + sendIdleStrategy.reset() + + // A kryo instance CANNOT be re-used until after it's buffer is flushed to the network! + val success = try { + // since ANY thread can call 'send', we have to take kryo instances in a safe way + val kryo = serialization.take() + try { + val buffer = kryo.write(connection, message) + val objectSize = buffer.position() + val internalBuffer = buffer.internalBuffer + + // one small problem! What if the message is too big to send all at once? + // The maximum size we can send in a "single fragment" is the maxPayloadLength() function, which is the MTU length less header (with defaults this is 1,376 bytes). + if (objectSize >= maxMessageSize) { + val kryoStream = serialization.take() + try { + // we must split up the message! It's too large for Aeron to manage. + streamingManager.send( + publication = publication, + originalBuffer = internalBuffer, + objectSize = objectSize, + maxMessageSize = maxMessageSize, + endPoint = this@EndPoint, + kryo = kryoStream, // this is safe, because we save out the bytes from the original object! + sendIdleStrategy = sendIdleStrategy, + connection = connection ) - ListenerManager.cleanStackTraceInternal(exception) - listenerManager.notifyError(exception) - throw exception - } - else { - // publication was actually closed, so no bother throwing an error - return + } finally { + serialization.put(kryoStream) } + } else { + aeronDriver.send(publication, internalBuffer, kryo.bufferClaim, 0, objectSize, sendIdleStrategy, connection, abortEarly, listenerManager) } - - /** - * The publication is not connected to a subscriber, this can be an intermittent state as subscribers come and go. - * val NOT_CONNECTED: Long = -1 - * - * The offer failed due to back pressure from the subscribers preventing further transmission. - * val BACK_PRESSURED: Long = -2 - * - * The offer failed due to an administration action and should be retried. - * The action is an operation such as log rotation which is likely to have succeeded by the next retry attempt. - * val ADMIN_ACTION: Long = -3 - */ - if (result >= Publication.ADMIN_ACTION) { - // we should retry. - handshakeSendIdleStrategy.idle() - continue - } - - // more critical error sending the message. we shouldn't retry or anything. - // this exception will be a ClientException or a ServerException - val exception = newException("[$aeronLogInfo] Error sending handshake message. $message (${errorCodeName(result)})") - ListenerManager.cleanStackTraceInternal(exception) - listenerManager.notifyError(exception) - throw exception + } finally { + serialization.put(kryo) } - } catch (e: Exception) { - if (e is ClientException || e is ServerException) { - throw e + } catch (e: Throwable) { + // if the driver is closed due to a network disconnect or a remote-client termination, we also must close the connection. + if (aeronDriver.internal.mustRestartDriverOnError) { + // we had a HARD network crash/disconnect, we close the driver and then reconnect automatically + //NOTE: notifyDisconnect IS NOT CALLED! + } + + // make sure we atomically create the listener manager, if necessary + else if (message is MethodResponse && message.result is Exception) { + val result = message.result as Exception + val newException = SerializationException("Error serializing message ${message.javaClass.simpleName}: '$message'", result) + listenerManager.notifyError(connection, newException) + } else if (message is ClientException || message is ServerException) { + val newException = TransmitException("Error with message ${message.javaClass.simpleName}: '$message'", e) + listenerManager.notifyError(connection, newException) } else { - val exception = newException("[$aeronLogInfo] Error serializing handshake message $message", e) - ListenerManager.cleanStackTrace(exception, 2) // 2 because we do not want to see the stack for the abstract `newException` - listenerManager.notifyError(exception) - throw exception + val newException = TransmitException("Error sending message ${message.javaClass.simpleName}: '$message'", e) + listenerManager.notifyError(connection, newException) } - } finally { - handshakeSendIdleStrategy.reset() + + false } + + return success } /** - * @param buffer The buffer - * @param offset The offset from the start of the buffer - * @param length The number of bytes to extract - * @param header The aeron header information + * This is designed to permit modifying/overriding how data is processed on the network. + * + * This will NOT split a message if it's too large. Aeron will just crash. This is used by the exclusively by the streaming manager. * - * @return the message + * @return true if the message was successfully sent by aeron, false otherwise. Exceptions are caught and NOT rethrown! */ - // note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD - internal fun readHandshakeMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header, aeronLogInfo: String): Any? { - return try { - // NOTE: This ABSOLUTELY MUST be done on the same thread! This cannot be done on a new one, because the buffer could change! - val message = handshakeKryo.read(buffer, offset, length) as HandshakeMessage - - logger.trace { "[$aeronLogInfo - ${message.connectKey}] received HS: $message" } + open fun writeUnsafe(message: Any, publication: Publication, sendIdleStrategy: IdleStrategy, connection: CONNECTION, kryo: KryoWriter): Boolean { + // NOTE: A kryo instance CANNOT be re-used until after it's buffer is flushed to the network! - message - } catch (e: Exception) { - // The handshake sessionId IS NOT globally unique - logger.error("[$aeronLogInfo] Error de-serializing message on connection ${header.sessionId()}!", e) - listenerManager.notifyError(e) - null - } + // since ANY thread can call 'send', we have to take kryo instances in a safe way + // the maximum size that this buffer can be is: + // ExpandableDirectByteBuffer.MAX_BUFFER_LENGTH = 1073741824 + val buffer = kryo.write(connection, message) + val objectSize = buffer.position() + val internalBuffer = buffer.internalBuffer + val bufferClaim = kryo.bufferClaim + + return aeronDriver.send(publication, internalBuffer, bufferClaim, 0, objectSize, sendIdleStrategy, connection, false, listenerManager) } /** - * read the message from the aeron buffer + * Processes a message that has been read off the network. * - * @param buffer The buffer - * @param offset The offset from the start of the buffer - * @param length The number of bytes to extract - * @param header The aeron header information - * @param connection The connection this message happened on + * The thread that reads this data, IS NOT the thread that consumes data off the network socket, but rather the data is consumed off + * of the logfile (hopefully on a mem-disk). This allows for backpressure and network messages to arrive **faster** that what can be + * processed. + * + * This is written in a way that permits modifying/overriding how data is processed on the network + * + * There are custom objects that are used (Ping, RmiMessages, Streaming object, etc.) are manage and use custom object types. These types + * must be EXPLICITLY used by the implementation, and if a custom message processor is to be used (ie: a state machine) you must + * guarantee that Ping, RMI, Streaming object, etc. are not used (as it would not function without this custom */ - internal fun processMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header, connection: Connection) { - // this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe! - @Suppress("UNCHECKED_CAST") - connection as CONNECTION - - try { - // NOTE: This ABSOLUTELY MUST be done on the same thread! This cannot be done on a new one, because the buffer could change! - val message = serialization.readMessage(buffer, offset, length, connection) - logger.trace { "[${header.sessionId()}] received: ${message?.javaClass?.simpleName} $message" } + open fun processMessage(message: Any?, connection: CONNECTION, readKryo: KryoReader) { + // the REPEATED usage of wrapping methods below is because Streaming messages have to intercept data BEFORE it goes to a coroutine + when (message) { + // the remote endPoint will send this message if it is closing the connection. + // IF we get this message in time, then we do not have to wait for the connection to expire before closing it + is DisconnectMessage -> { + val closeEverything = message.closeEverything + + if (logger.isDebugEnabled) { + if (closeEverything) { + logger.debug("Received disconnect message from $otherTypeName") + } else { + logger.debug("Received session disconnect message from $otherTypeName") + } + } - // the REPEATED usage of wrapping methods below is because Streaming messages have to intercept date BEFORE it goes to a coroutine + // make sure we flag the connection as NOT to timeout!! + connection.isClosedWithTimeout() // we only need this to update fields + connection.close(sendDisconnectMessage = false, closeEverything = closeEverything) + } - when (message) { - is CloseMessage -> { - // immediately close the connection - connection.close() + // streaming message. This is used when the published data is too large for a single Aeron message. + // TECHNICALLY, we could arbitrarily increase the size of the permitted Aeron message, however this doesn't let us + // send arbitrarily large pieces of data (gigs in size, potentially). + // This will recursively call into this method for each of the unwrapped blocks of data. + is StreamingControl -> { + // NOTE: this CANNOT be on a separate threads, because we must guarantee that this happens first! + streamingManager.processControlMessage(message, this@EndPoint, connection) + } + is StreamingData -> { + // NOTE: this CANNOT be on a separate threads, because we must guarantee that this happens in-order! + try { + streamingManager.processDataMessage(message, this@EndPoint, connection) + } catch (e: Exception) { + listenerManager.notifyError(connection, StreamingException("Error processing StreamingMessage", e)) } + } - is Ping -> { - // NOTE: This MUST be on a new co-routine - actionDispatch.launch { - try { - pingManager.manage(connection, responseManager, message, logger) - } catch (e: Exception) { - logger.error("Error processing message", e) - listenerManager.notifyError(connection, e) - } - } - } + is Any -> { + // NOTE: This MUST be on a new threads (otherwise RMI has issues where it will be blocking) + val paired = pairedPool.take() + paired.connection = connection + paired.message = message - // small problem... If we expect IN ORDER messages (ie: setting a value, then later reading the value), multiple threads don't work. - // this is worked around by having RMI always return (unless async), even with a null value, so the CALLING side of RMI will always - // go in "lock step" - is RmiMessage -> { - // if we are an RMI message/registration, we have very specific, defined behavior. - // We do not use the "normal" listener callback pattern because this requires special functionality - // NOTE: This MUST be on a new co-routine - actionDispatch.launch { - try { - rmiGlobalSupport.processMessage(serialization, connection, message, rmiConnectionSupport, responseManager, logger) - } catch (e: Exception) { - logger.error("Error processing message", e) - listenerManager.notifyError(connection, e) - } - } - } + // This will try to send the element (blocking if necessary) + messageChannel.trySendBlocking(paired) + } - // streaming/chunked message. This is used when the published data is too large for a single Aeron message. - // TECHNICALLY, we could arbitrarily increase the size of the permitted Aeron message, however this doesn't let us - // send arbitrarily large pieces of data (gigs in size, potentially). - // This will recursively call into this method for each of the unwrapped chunks of data. - is StreamingControl -> { - streamingManager.processControlMessage(message, this@EndPoint, connection) - } - is StreamingData -> { - // NOTE: this will read extra data from the kryo input as necessary (which is why it's not on action dispatch)! - val rawInput = serialization.readRaw() - val dataLength = rawInput.readVarInt(true) - message.payload = rawInput.readBytes(dataLength) + else -> { + listenerManager.notifyError(connection, MessageDispatchException("Unknown message received!!")) + } + } + } + /** + * This is also what process the incoming message when it is received from the aeron network. + * + * THIS IS PROCESSED ON MULTIPLE THREADS! + */ + internal fun processMessageFromChannel(connection: CONNECTION, message: Any) { + when (message) { + is Ping -> { + // PING will also measure APP latency, not just NETWORK PIPE latency + try { + connection.receivePing(message) + } catch (e: Exception) { + listenerManager.notifyError(connection, PingException("Error while processing Ping message: $message", e)) + } + } - // NOTE: This MUST NOT be on a new co-routine. It must be on the same thread! - try { - streamingManager.processDataMessage(message, this@EndPoint) - } catch (e: Exception) { - logger.error("Error processing StreamingMessage", e) - listenerManager.notifyError(connection, e) - } + is BufferedMessages -> { + // this can potentially be an EXTREMELY large set of data -- so when there are buffered messages, it is often better + // to batch-send them instead of one-at-a-time (which can cause excessive CPU load and Network I/O) + message.messages.forEach { + processMessageFromChannel(connection, it) } + } + // small problem... If we expect IN ORDER messages (ie: setting a value, then later reading the value), multiple threads don't work. + // this is worked around by having RMI always return (unless async), even with a null value, so the CALLING side of RMI will always + // go in "lock step" + is RmiMessage -> { + // if we are an RMI message/registration, we have very specific, defined behavior. + // We do not use the "normal" listener callback pattern because this requires special functionality + try { + rmiGlobalSupport.processMessage(serialization, connection, message, rmiConnectionSupport, responseManager, logger) + } catch (e: Exception) { + listenerManager.notifyError(connection, RMIException("Error while processing RMI message", e)) + } + } - is Any -> { - // NOTE: This MUST be on a new co-routine - actionDispatch.launch { + is SendSync -> { + // SendSync enables us to NOTIFY the remote endpoint that we have received the message. This is to guarantee happens-before! + // Using this will depend upon APP+NETWORK latency, and is (by design) not as performant as sending a regular message! + try { + val message2 = message.message + if (message2 != null) { + // this is on the "remote end". Make sure to dispatch/notify the message BEFORE we send a message back! try { - var hasListeners = listenerManager.notifyOnMessage(connection, message) + var hasListeners = listenerManager.notifyOnMessage(connection, message2) // each connection registers, and is polled INDEPENDENTLY for messages. - hasListeners = hasListeners or connection.notifyOnMessage(message) + hasListeners = hasListeners or connection.notifyOnMessage(message2) if (!hasListeners) { - logger.error("No message callbacks found for ${message::class.java.name}") + listenerManager.notifyError(connection, MessageDispatchException("No send-sync message callbacks found for ${message2::class.java.name}")) } } catch (e: Exception) { - logger.error("Error processing message ${message::class.java.name}", e) - listenerManager.notifyError(connection, e) + listenerManager.notifyError(connection, MessageDispatchException("Error processing send-sync message ${message2::class.java.name}", e)) } } + + connection.receiveSendSync(message) + } catch (e: Exception) { + listenerManager.notifyError(connection, SendSyncException("Error while processing send-sync message: $message", e)) } + } - else -> { - logger.error("Unknown message received!!") + else -> { + try { + var hasListeners = listenerManager.notifyOnMessage(connection, message) + + // each connection registers, and is polled INDEPENDENTLY for messages. + hasListeners = hasListeners or connection.notifyOnMessage(message) + + if (!hasListeners) { + listenerManager.notifyError(connection, MessageDispatchException("No message callbacks found for ${message::class.java.name}")) + } + } catch (e: Exception) { + listenerManager.notifyError(connection, MessageDispatchException("Error processing message ${message::class.java.name}", e)) } } - } catch (e: Exception) { - // The handshake sessionId IS NOT globally unique - logger.error("[${header.sessionId()}] Error de-serializing message", e) - listenerManager.notifyError(connection, e) } } + /** - * NOTE: this **MUST** stay on the same thread/coroutine that calls "send". This cannot be re-dispatched onto a different coroutine! + * reads the message from the aeron buffer and figures out how to process it. * - * @return true if the message was successfully sent by aeron, false otherwise. Exceptions are caught and NOT rethrown! + * This can be overridden should you want to customize exactly how data is received. + * + * @param buffer The buffer + * @param offset The offset from the start of the buffer + * @param length The number of bytes to extract + * @param header The aeron header information + * @param connection The connection this message happened on */ - @Suppress("DuplicatedCode", "UNCHECKED_CAST") - internal fun send(message: Any, publication: Publication, connection: Connection): Boolean { - // The handshake sessionId IS NOT globally unique - logger.trace { "[${publication.sessionId()}] send: ${message.javaClass.simpleName} : $message" } - + internal fun dataReceive( + buffer: DirectBuffer, + offset: Int, + length: Int, + header: Header, + connection: Connection + ) { + // this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe! + @Suppress("UNCHECKED_CAST") connection as CONNECTION - // since ANY thread can call 'send', we have to take kryo instances in a safe way - val kryo: KryoExtra = serialization.takeKryo() try { - // the maximum size that this buffer can be is: - // ExpandableDirectByteBuffer.MAX_BUFFER_LENGTH = 1073741824 - val buffer = kryo.write(connection, message) - val objectSize = buffer.position() - val internalBuffer = buffer.internalBuffer - - - // one small problem! What if the message is too big to send all at once? - val maxMessageLength = publication.maxMessageLength() - if (objectSize >= maxMessageLength) { - // we must split up the message! It's too large for Aeron to manage. - // this will split up the message, construct the necessary control message and state, then CALL the sendData - // method directly for each subsequent message. - return streamingManager.send(publication, internalBuffer, - objectSize, this, connection) + // NOTE: This ABSOLUTELY MUST be done on the same thread! This cannot be done on a new one, because the buffer could change! + val message = readKryo.read(buffer, offset, length, connection) + if (logger.isTraceEnabled) { + // don't automatically create the lambda when trace is disabled! Because this uses 'outside' scoped info, it's a new lambda each time! + logger.trace("[${header.sessionId()}] received: ${message?.javaClass?.simpleName} $message") } - - return sendData(publication, internalBuffer, 0, objectSize, connection) + processMessage(message, connection, readKryo) } catch (e: Exception) { - if (message is MethodResponse && message.result is Exception) { - val result = message.result as Exception - logger.error("[${publication.sessionId()}] Error serializing message '$message'", result) - listenerManager.notifyError(connection, result) - } else if (message is ClientException || message is ServerException) { - logger.error("[${publication.sessionId()}] Error for message '$message'", e) - listenerManager.notifyError(connection, e) - } else { - logger.error("[${publication.sessionId()}] Error serializing message '$message'", e) - listenerManager.notifyError(connection, e) - } - } finally { - sendIdleStrategy.reset() - serialization.returnKryo(kryo) + listenerManager.notifyError(connection, newException("Error de-serializing message", e)) } - - return false } /** - * the actual bits that send data on the network. + * Ensures that an endpoint (using the specified configuration) is NO LONGER running. * - * @return true if the message was successfully sent by aeron, false otherwise. Exceptions are caught and NOT rethrown! + * By default, we will wait the [Configuration.connectionCloseTimeoutInSeconds] * 2 amount of time before returning, and + * 50ms between checks of the endpoint running + * + * @return true if the media driver is STOPPED. */ - internal fun sendData(publication: Publication, internalBuffer: MutableDirectBuffer, offset: Int, objectSize: Int, connection: CONNECTION): Boolean { - var timeoutInNanos = 0L - var startTime = 0L - - var result: Long - while (true) { - result = publication.offer(internalBuffer, offset, objectSize) - if (result >= 0) { - // success! - return true - } - - /** - * Since the publication is not connected, we weren't able to send data to the remote endpoint. - * - * According to Aeron Docs, Pubs and Subs can "come and go", whatever that means. We just want to make sure that we - * don't "loop forever" if a publication is ACTUALLY closed, like on purpose. - */ - if (result == Publication.NOT_CONNECTED) { - if (timeoutInNanos == 0L) { - timeoutInNanos = (aeronDriver.getLingerNs() * 1.2).toLong() // close enough. Just needs to be slightly longer - startTime = System.nanoTime() - } - - if (System.nanoTime() - startTime < timeoutInNanos) { - // we should retry. - sendIdleStrategy.idle() - continue - } else if (!publication.isClosed) { - // more critical error sending the message. we shouldn't retry or anything. - val errorMessage = "[${publication.sessionId()}] Error sending message. (Connection in non-connected state longer than linger timeout. ${errorCodeName(result)})" - - // either client or server. No other choices. We create an exception, because it's more useful! - val exception = newException(errorMessage) - - // +2 because we do not want to see the stack for the abstract `newException` - // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is - // where we see who is calling "send()" - ListenerManager.cleanStackTrace(exception, 5) - return false - } else { - // publication was actually closed, so no bother throwing an error - return false - } - } - - /** - * The publication is not connected to a subscriber, this can be an intermittent state as subscribers come and go. - * val NOT_CONNECTED: Long = -1 - * - * The offer failed due to back pressure from the subscribers preventing further transmission. - * val BACK_PRESSURED: Long = -2 - * - * The offer failed due to an administration action and should be retried. - * The action is an operation such as log rotation which is likely to have succeeded by the next retry attempt. - * val ADMIN_ACTION: Long = -3 - */ - if (result >= Publication.ADMIN_ACTION) { - // we should retry, BUT we want to suspend ANYONE ELSE trying to write at the same time! - sendIdleStrategy.idle() - continue - } - - - if (result == Publication.CLOSED && connection.isClosedViaAeron()) { - // this can happen when we use RMI to close a connection. RMI will (in most cases) ALWAYS send a response when it's - // done executing. If the connection is *closed* first (because an RMI method closed it), then we will not be able to - // send the message. - // NOTE: we already know the connection is closed. we closed it (so it doesn't make sense to emit an error about this) - return false - } - - // more critical error sending the message. we shouldn't retry or anything. - val errorMessage = "[${publication.sessionId()}] Error sending message. (${errorCodeName(result)})" - - // either client or server. No other choices. We create an exception, because it's more useful! - val exception = newException(errorMessage) + fun ensureStopped(timeoutMS: Long = TimeUnit.SECONDS.toMillis(config.connectionCloseTimeoutInSeconds.toLong() * 2), + intervalTimeoutMS: Long = 500): Boolean { - // +2 because we do not want to see the stack for the abstract `newException` - // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is - // where we see who is calling "send()" - ListenerManager.cleanStackTrace(exception, 5) - return false - } + return aeronDriver.ensureStopped(timeoutMS, intervalTimeoutMS) } /** @@ -784,16 +815,34 @@ internal constructor(val type: Class<*>, return aeronDriver.driverBacklog() } + /** + * @param errorAction callback for each of the errors reported by the Aeron driver in the current Aeron directory + */ + fun driverErrors(errorAction: (observationCount: Int, firstObservationTimestamp: Long, lastObservationTimestamp: Long, encodedException: String) -> Unit) { + aeronDriver.driverErrors(errorAction) + } + + /** + * @param lossStats callback for each of the loss statistic entries reported by the Aeron driver in the current Aeron directory + */ + fun driverLossStats(lossStats: (observationCount: Long, + totalBytesLost: Long, + firstObservationTimestamp: Long, + lastObservationTimestamp: Long, + sessionId: Int, streamId: Int, + channel: String, source: String) -> Unit): Int { + return aeronDriver.driverLossStats(lossStats) + } /** - * @return the internal heartbeat of the Aeron driver in the current aeron directory + * @return the internal heartbeat of the Aeron driver in the current Aeron directory */ fun driverHeartbeatMs(): Long { return aeronDriver.driverHeartbeatMs() } /** - * @return the internal version of the Aeron driver in the current aeron directory + * @return the internal version of the Aeron driver in the current Aeron directory */ fun driverVersion(): String { return aeronDriver.driverVersion() @@ -814,91 +863,269 @@ internal constructor(val type: Class<*>, } /** - * Waits for this endpoint to be closed + * Waits for this endpoint to be internally shutdown, but not 100% fully closed (which only happens manually) + * + * @return true if the wait completed before the timeout + */ + internal fun waitForEndpointShutdown(timeoutMS: Long = 0L): Boolean { + // default is true, because if we haven't started up yet, we don't even check the latches + var success = true + + + var origPollerLatch: CountDownLatch? + var origShutdownLatch: CountDownLatch? = null + + + // don't need to check for both, as they are set together (we just have to check the later of the two) + while (origShutdownLatch !== shutdownLatch) { + // if we redefine the latches WHILE we are waiting for them, then we will NEVER release (since we lose the reference to the + // original latch). This makes sure to check again to make sure we don't appear to deadlock + origPollerLatch = pollerClosedLatch + origShutdownLatch = shutdownLatch + + + if (timeoutMS > 0) { + success = success && origPollerLatch.await(timeoutMS, TimeUnit.MILLISECONDS) + success = success && origShutdownLatch.await(timeoutMS, TimeUnit.MILLISECONDS) + } else { + origPollerLatch.await() + origShutdownLatch.await() + success = true + } + } + + return success + } + + + /** + * Waits for this endpoint to be fully closed. A disconnect from the network (or remote endpoint) will not signal this to continue. + */ + fun waitForClose(): Boolean { + return waitForClose(0L) + } + + /** + * Waits for this endpoint to be fully closed. A disconnect from the network (or remote endpoint) will not signal this to continue. + * + * @return true if the wait completed before the timeout */ - fun waitForClose() { - var latch: CountDownLatch? = null + fun waitForClose(timeoutMS: Long = 0L): Boolean { + // if we are restarting the network state, we want to continue to wait for a proper close event. + // when shutting down, it can take up to 5 seconds to fully register as "shutdown" - while (latch !== shutdownLatch) { - latch = shutdownLatch - // if we are restarting the network state, we want to continue to wait for a proper close event. Because we RESET the latch, - // we must continue to check - latch.await() + if (networkEventPoller.isDispatch()) { + // we cannot wait for a connection while inside the network event dispatch, since it has to close itself and this method waits for it!! + throw IllegalStateException("Unable to 'waitForClose()' while inside the network event dispatch, this will deadlock!") } + + logger.trace("Waiting for endpoint to close...") + + var origCloseLatch: CountDownLatch? = null + + var success = false + while (origCloseLatch !== closeLatch) { + // if we redefine the latches WHILE we are waiting for them, then we will NEVER release (since we lose the reference to the + // original latch). This makes sure to check again to make sure we don't appear to deadlock + origCloseLatch = closeLatch + + + success = if (timeoutMS > 0) { + origCloseLatch.await(timeoutMS, TimeUnit.MILLISECONDS) + } else { + origCloseLatch.await() + true + } + } + + return success } - final override fun close() { - if (shutdown.compareAndSet(expect = false, update = true)) { - logger.info { "Shutting down..." } + /** + * Shall we preserve state when we shutdown, or do we remove all onConnect/Disconnect/etc events from memory. + * + * There are two viable concerns when we close the connection/client. + * 1) We should reset 100% of the state+events, so that every time we connect, everything is redone + * 2) We preserve the state+event, BECAUSE adding the onConnect/Disconnect/message event states might be VERY expensive. + * + * NOTE: This method does NOT block, as the connection state is asynchronous. Use "waitForClose()" to wait for this to finish. + * + * This will unblock the tread waiting in "waitForClose()" when it is finished. + * + * @param closeEverything true only possible via the Client.close() or Server.close() methods. + */ + internal fun close( + closeEverything: Boolean, + sendDisconnectMessage: Boolean, + releaseWaitingThreads: Boolean, + redispatched: Boolean = false) + { + if (isShutdown()) { + // we have already closed! Don't try to close again + logger.debug("Already shutting down endpoint, skipping multiple attempts...") + return + } - // the server has to be able to call server.notifyDisconnect() on a list of connections. If we remove the connections - // inside of connection.close(), then the server does not have a list of connections to call the global notifyDisconnect() - val enableRemove = type == Client::class.java - connections.forEach { - logger.info { "[${it.id}/${it.streamId}] Closing connection" } - it.close(enableRemove) + if (!eventDispatch.CLOSE.isDispatch()) { + eventDispatch.CLOSE.launch { + // only time the redispatch is true! + close(closeEverything, sendDisconnectMessage, releaseWaitingThreads, true) } - runBlocking { - // Connections are closed first, because we want to make sure that no RMI messages can be received - // when we close the RMI support objects (in which case, weird - but harmless - errors show up) - // this will wait for RMI timeouts if there are RMI in-progress. (this happens if we close via and RMI method) - responseManager.close() + if (closeEverything) { + waitForClose() + shutdownEventDispatcher() // once shutdown, it cannot be restarted! } - // the storage is closed via this as well. + logger.info("Done shutting down the endpoint.") + + return + } + + if (logger.isDebugEnabled) { + logger.debug("Requesting close: closeEverything=$closeEverything, sendDisconnectMessage=$sendDisconnectMessage, releaseWaitingThreads=$releaseWaitingThreads") + } + + // 1) endpoints can call close() + // 2) client can close the endpoint if the connection is D/C from aeron (and the endpoint was not closed manually) + val shutdownPreviouslyStarted = shutdownInProgress.getAndSet(true) + if (closeEverything && shutdownPreviouslyStarted) { + if (logger.isDebugEnabled) { + logger.debug("Shutdown previously started, cleaning up...") + } + // this is only called when the client network event poller shuts down + // if we have clientConnectionClosed, then run that logic (because it doesn't run on the client when the connection is closed remotely) + + // Clears out all registered events + listenerManager.close() + + // Remove from memory the data from the back-end storage storage.close() - close0() + // don't do anything more, since we've already shutdown! + return + } - aeronDriver.close() + if (shutdownPreviouslyStarted) { + if (logger.isDebugEnabled) { + logger.debug("Shutdown previously started, ignoring...") + } + return + } - shutdownLatch = CountDownLatch(1) + if (Thread.currentThread() != hook) { + try { + Runtime.getRuntime().removeShutdownHook(hook) + } catch (ignored: Exception) { + } catch (ignored: RuntimeException) { + } + } - // if we are waiting for shutdown, cancel the waiting thread (since we have shutdown now) - shutdownLatch.countDown() + logger.debug("Shutting down endpoint...") + shutdown.lazySet(true) - logger.info { "Done shutting down..." } + // always do this. It is OK to run this multiple times + // the server has to be able to call server.notifyDisconnect() on a list of connections. If we remove the connections + // inside of connection.close(), then the server does not have a list of connections to call the global notifyDisconnect() + connections.forEach { + it.closeImmediately(sendDisconnectMessage = sendDisconnectMessage, closeEverything = closeEverything) } - } - /** - * Close in such a way that we enable us to be restarted. This is the same as a "normal close", but DOES NOT close - * - response manager - * - storage - */ - fun closeForRestart() { - if (shutdown.compareAndSet(expect = false, update = true)) { - logger.info { "Shutting down for restart..." } + if (this is Client<*>) { + // if there is a client connection IN PROGRESS... then we must wait for that to timeout so we can make sure everything is closed in the right order. + clientConnectionInProgress.await() + } - // the server has to be able to call server.notifyDisconnect() on a list of connections. If we remove the connections - // inside of connection.close(), then the server does not have a list of connections to call the global notifyDisconnect() - val enableRemove = type == Client::class.java - connections.forEach { - logger.info { "[${it.id}/${it.streamId}] Closing connection" } - it.close(enableRemove) - } + // this closes the endpoint specific instance running in the poller - close0() + // THIS WILL SHUT DOWN THE EVENT POLLER IMMEDIATELY! BUT IN AN ASYNC MANNER! + shutdownEventPoller = true - aeronDriver.close() + // if we close the poller AND listener manager too quickly, events will not get published + // this waits for the ENDPOINT to finish running its tasks in the poller. + pollerClosedLatch.await() + + + + // this will ONLY close the event dispatcher if ALL endpoints have closed it. + // when an endpoint closes, the poll-loop shuts down, and removes itself from the list of poll actions that need to be performed. + networkEventPoller.close(logger, this) + + // Connections MUST be closed first, because we want to make sure that no RMI messages can be received + // when we close the RMI support objects (in which case, weird - but harmless - errors show up) + // IF CLOSED VIA RMI: this will wait for RMI timeouts if there are RMI in-progress. + if (closeEverything) { + // only close out RMI if we are closing everything! + responseManager.close(logger) + } - shutdownLatch = CountDownLatch(1) - // if we are waiting for shutdown, cancel the waiting thread (since we have shutdown now) - shutdownLatch.countDown() + // don't do these things if we are "closed" from a client connection disconnect + // if there are any events going on, we want to schedule them to run AFTER all other events for this endpoint are done + if (closeEverything) { + // when the client connection is closed, we don't close the driver/etc. - logger.info { "Done shutting down for restart..." } + // Clears out all registered events + listenerManager.close() + + // Remove from memory the data from the back-end storage + storage.close() + } + + // we might be restarting the aeron driver, so make sure it's closed. + aeronDriver.close() + + // the shutdown here must be in the launchSequentially lambda, this way we can guarantee the driver is closed before we move on + shutdownInProgress.lazySet(false) + shutdownLatch.countDown() + + if (releaseWaitingThreads) { + logger.trace("Counting down the close latch...") + closeLatch.countDown() + } + + if (!redispatched) { + if (closeEverything) { + waitForClose() + shutdownEventDispatcher() // once shutdown, it cannot be restarted! + } + + logger.info("Done shutting down the endpoint.") } } - internal open fun close0() {} + /** + * @return true if the current execution thread is in the primary network event dispatch + */ + fun isDispatch(): Boolean { + return networkEventPoller.isDispatch() + } + /** + * Shuts-down each event dispatcher executor, and waits for it to gracefully shutdown. + * + * Once shutdown, it cannot be restarted and the application MUST recreate the endpoint + * + * @param timeout how long to wait, must be > 0 + * @param timeoutUnit what the unit count is + */ + fun shutdownEventDispatcher(timeout: Long = 15, timeoutUnit: TimeUnit = TimeUnit.SECONDS) { + logger.debug("Waiting for Event Dispatcher to shutdown...") + eventDispatch.shutdownAndWait(timeout, timeoutUnit) + logger.info("Done shutting down Event Dispatcher...") + } - override fun toString(): String { - return "EndPoint [${type.simpleName}]" + /** + * Reset the running state when there's an error starting up + */ + internal fun resetOnError() { + shutdownLatch.countDown() + pollerClosedLatch.countDown() + endpointIsRunning.lazySet(false) + shutdown.lazySet(false) + shutdownEventPoller = false } override fun hashCode(): Int { diff --git a/src/dorkbox/network/connection/EventDispatcher.kt b/src/dorkbox/network/connection/EventDispatcher.kt new file mode 100644 index 00000000..8c50874f --- /dev/null +++ b/src/dorkbox/network/connection/EventDispatcher.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection + +import dorkbox.network.Configuration +import dorkbox.util.NamedThreadFactory +import kotlinx.atomicfu.atomic +import org.slf4j.LoggerFactory +import java.util.concurrent.* + +/** + * Event logic throughout the network MUST be run on multiple threads! There are deadlock issues if it is only one, or if the client + server + * share an event dispatcher (multiple network restarts were required to check this) + * + * WARNING: The logic in this class will ONLY work in this class, as it relies on this specific behavior. Do not use it elsewhere! + */ +internal class EventDispatcher(val type: String) { + enum class EDType { + // CLOSE must be last! + HANDSHAKE, CONNECT, ERROR, CLOSE + } + + internal class ED(private val dispatcher: EventDispatcher, private val type: EDType) { + fun launch(function: () -> Unit) { + dispatcher.launch(type, function) + } + + fun isDispatch(): Boolean { + return dispatcher.isDispatch(type) + } + + fun shutdownAndWait(timeout: Long, timeoutUnit: TimeUnit) { + dispatcher.shutdownAndWait(type, timeout, timeoutUnit) + } + } + + companion object { + private val DEBUG_EVENTS = false + private val traceId = atomic(0) + + private val typedEntries: Array + + init { + typedEntries = EDType.entries.toTypedArray() + } + } + + private val logger = LoggerFactory.getLogger("$type Dispatch") + + private val threadIds = EDType.entries.map { atomic(0L) }.toTypedArray() + + private val executors = EDType.entries.map { event -> + // It CANNOT be the default dispatch because there will be thread starvation + // NOTE: THIS CANNOT CHANGE!! IT WILL BREAK EVERYTHING IF IT CHANGES! + Executors.newSingleThreadExecutor( + NamedThreadFactory( + namePrefix = "$type-${event.name}", + group = Configuration.networkThreadGroup, + threadPriority = Thread.NORM_PRIORITY, + daemon = true + ) { thread -> + // when a new thread is created, assign it to the array + threadIds[event.ordinal].lazySet(thread.id) + } + ) + }.toTypedArray() + + val HANDSHAKE: ED + val CONNECT: ED + val ERROR: ED + val CLOSE: ED + + + init { + executors.forEachIndexed { _, executor -> + executor.submit { + // this is to create a new thread only, so that the thread ID can be assigned + } + } + + HANDSHAKE = ED(this, EDType.HANDSHAKE) + CONNECT = ED(this, EDType.CONNECT) + ERROR = ED(this, EDType.ERROR) + CLOSE = ED(this, EDType.CLOSE) + } + + + /** + * Shuts-down each event dispatcher executor, and waits for it to gracefully shutdown. Once shutdown, it cannot be restarted. + * + * @param timeout how long to wait + * @param timeoutUnit what the unit count is + */ + fun shutdownAndWait(timeout: Long, timeoutUnit: TimeUnit) { + require(timeout > 0) { logger.error("The EventDispatcher shutdown timeout must be > 0!") } + + HANDSHAKE.shutdownAndWait(timeout, timeoutUnit) + CONNECT.shutdownAndWait(timeout, timeoutUnit) + ERROR.shutdownAndWait(timeout, timeoutUnit) + CLOSE.shutdownAndWait(timeout, timeoutUnit) + } + + /** + * Checks if the current execution thread is running inside one of the event dispatchers. + */ + fun isDispatch(): Boolean { + val threadId = Thread.currentThread().id + + typedEntries.forEach { event -> + if (threadIds[event.ordinal].value == threadId) { + return true + } + } + + return false + } + + /** + * Checks if the current execution thread is running inside one of the event dispatchers. + */ + private fun isDispatch(type: EDType): Boolean { + val threadId = Thread.currentThread().id + + return threadIds[type.ordinal].value == threadId + } + + /** + * shuts-down the current execution thread and waits for it complete. + */ + private fun shutdownAndWait(type: EDType, timeout: Long, timeoutUnit: TimeUnit) { + executors[type.ordinal].shutdown() + executors[type.ordinal].awaitTermination(timeout, timeoutUnit) + } + + /** + * Each event type runs inside its own thread executor. + * + * We want EACH event type to run in its own executor... on its OWN thread, in order to prevent deadlocks + * This is because there are blocking dependencies: DISCONNECT -> CONNECT. + * + * If an event is RE-ENTRANT, then it will immediately execute! + */ + private fun launch(event: EDType, function: () -> Unit) { + val eventId = event.ordinal + + try { + if (DEBUG_EVENTS) { + val id = traceId.getAndIncrement() + executors[eventId].submit { + if (logger.isDebugEnabled) { + logger.debug("Starting $event : $id") + } + function() + if (logger.isDebugEnabled) { + logger.debug("Finished $event : $id") + } + } + } else { + executors[eventId].submit(function) + } + } catch (e: Exception) { + logger.error("Error during event dispatch!", e) + } + } +} diff --git a/src/dorkbox/network/connection/IpInfo.kt b/src/dorkbox/network/connection/IpInfo.kt new file mode 100644 index 00000000..afe4573d --- /dev/null +++ b/src/dorkbox/network/connection/IpInfo.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection + +import dorkbox.netUtil.IPv4 +import dorkbox.netUtil.IPv6 +import dorkbox.network.ServerConfiguration +import dorkbox.network.connection.IpInfo.Companion.IpListenType.IPC +import dorkbox.network.connection.IpInfo.Companion.IpListenType.IPWildcard +import dorkbox.network.connection.IpInfo.Companion.IpListenType.IPv4Wildcard +import dorkbox.network.connection.IpInfo.Companion.IpListenType.IPv6Wildcard +import java.net.Inet4Address +import java.net.Inet6Address +import java.net.InetAddress + +internal class IpInfo(config: ServerConfiguration) { + + + companion object { + enum class IpListenType { + IPv4, IPv6, IPv4Wildcard, IPv6Wildcard, IPWildcard, IPC + } + + fun isLocalhost(ipAddress: String): Boolean { + return when (ipAddress.lowercase()) { + "loopback", "localhost", "lo", "127.0.0.1", "::1" -> true + else -> false + } + } + fun isWildcard(ipAddress: String): Boolean { + return when (ipAddress) { + // this is the "wildcard" address. Windows has problems with this. + "0", "::", "0.0.0.0", "*" -> true + else -> false + } + } + + fun isWildcard(ipAddress: InetAddress): Boolean { + return when (ipAddress) { + // this is the "wildcard" address. Windows has problems with this. + IPv4.WILDCARD, IPv6.WILDCARD -> true + else -> false + } + } + + fun getWildcard(ipAddress: InetAddress, ipAddressString: String, shouldBeIpv4: Boolean): String { + return if (isWildcard(ipAddress)) { + if (shouldBeIpv4) { + IPv4.WILDCARD_STRING + } else { + IPv6.WILDCARD_STRING + } + } else { + ipAddressString + } + } + + fun formatCommonAddress(ipAddress: String, isIpv4: Boolean, elseAction: () -> InetAddress?): InetAddress? { + return if (isLocalhost(ipAddress)) { + if (isIpv4) { IPv4.LOCALHOST } else { IPv6.LOCALHOST } + } else if (isWildcard(ipAddress)) { + if (isIpv4) { IPv4.WILDCARD } else { IPv6.WILDCARD } + } else if (IPv4.isValid(ipAddress)) { + IPv4.toAddress(ipAddress)!! + } else if (IPv6.isValid(ipAddress)) { + IPv6.toAddress(ipAddress)!! + } else { + elseAction() + } + } + + fun formatCommonAddressString(ipAddress: String, isIpv4: Boolean, elseAction: () -> String = { ipAddress }): String { + return if (isLocalhost(ipAddress)) { + if (isIpv4) { IPv4.LOCALHOST_STRING } else { IPv6.LOCALHOST_STRING } + } else if (isWildcard(ipAddress)) { + if (isIpv4) { IPv4.WILDCARD_STRING } else { IPv6.WILDCARD_STRING } + } else if (IPv4.isValid(ipAddress)) { + ipAddress + } else if (IPv6.isValid(ipAddress)) { + ipAddress + } else { + elseAction() + } + } + } + + + + + val ipType: IpListenType + + + val listenAddress: InetAddress? + val listenAddressString: String + val formattedListenAddressString: String + val listenAddressStringPretty: String + val isReliable = config.isReliable + + val isIpv4: Boolean + + init { + val canUseIPv4 = config.enableIPv4 && IPv4.isAvailable + val canUseIPv6 = config.enableIPv6 && IPv6.isAvailable + + // localhost/loopback IP might not always be 127.0.0.1 or ::1 + // We want to listen on BOTH IPv4 and IPv6 (config option lets us configure this) we listen in IPv6 WILDCARD + var listenAddress: InetAddress? + var ip46Wildcard = false + + when { + canUseIPv4 && canUseIPv6 -> { + // if it's not a valid IP, the lambda will return null + listenAddress = formatCommonAddress(config.listenIpAddress, false) { null } + + if (listenAddress == null) { + listenAddress = formatCommonAddress(config.listenIpAddress, true) { null } + } else { + ip46Wildcard = true + } + } + canUseIPv4 -> { + // if it's not a valid IP, the lambda will return null + listenAddress = formatCommonAddress(config.listenIpAddress, true) { null } + } + canUseIPv6 -> { + // if it's not a valid IP, the lambda will return null + listenAddress = formatCommonAddress(config.listenIpAddress, false) { null } + } + else -> { + listenAddress = null + } + } + + this.listenAddress = listenAddress + + + isIpv4 = listenAddress is Inet4Address + + // if we are IPv6 WILDCARD -- then our listen-address must ALSO be IPv6, even if our connection is via IPv4 + when (listenAddress) { + IPv6.WILDCARD -> { + ipType = if (ip46Wildcard) { + IPWildcard + } else { + IPv6Wildcard + } + + listenAddressString = IPv6.WILDCARD_STRING + formattedListenAddressString = if (listenAddressString[0] == '[') { + listenAddressString + } else { + // there MUST be [] surrounding the IPv6 address for aeron to like it! + "[$listenAddressString]" + } + } + IPv4.WILDCARD -> { + ipType = IPv4Wildcard + listenAddressString = IPv4.WILDCARD_STRING + formattedListenAddressString = listenAddressString + } + is Inet6Address -> { + ipType = IpListenType.IPv6 + listenAddressString = IPv6.toString(listenAddress) + formattedListenAddressString = if (listenAddressString[0] == '[') { + listenAddressString + } else { + // there MUST be [] surrounding the IPv6 address for aeron to like it! + "[$listenAddressString]" + } + } + is Inet4Address -> { + ipType = IpListenType.IPv4 + listenAddressString = IPv4.toString(listenAddress) + formattedListenAddressString = listenAddressString + } + else -> { + ipType = IPC + listenAddressString = EndPoint.IPC_NAME + formattedListenAddressString = listenAddressString + } + } + + listenAddressStringPretty = when (listenAddress) { + IPv4.WILDCARD -> listenAddressString + IPv6.WILDCARD -> IPv4.WILDCARD.hostAddress + "/" + listenAddressString + else -> listenAddressString + } + } + + /** + * if we are listening on :: (ipv6), and a connection via ipv4 arrives, aeron MUST publish on the IPv4 version + */ + fun getAeronPubAddress(remoteIpv4: Boolean): String { + return if (remoteIpv4) { + when (ipType) { + IPWildcard -> IPv4.WILDCARD_STRING + else -> formattedListenAddressString + } + } else { + formattedListenAddressString + } + } + + /** + * if we are listening on :: (ipv6), and a connection via ipv4 arrives, aeron MUST publish on the IPv6 version + */ + fun getAeronSubAddress(remoteIpv4: Boolean): String { + return if (remoteIpv4) { + when (ipType) { + IPWildcard -> IPv4.WILDCARD_STRING + else -> formattedListenAddressString + } + } else { + formattedListenAddressString + } + } +} diff --git a/src/dorkbox/network/connection/ListenerManager.kt b/src/dorkbox/network/connection/ListenerManager.kt index 95bdfb49..da274feb 100644 --- a/src/dorkbox/network/connection/ListenerManager.kt +++ b/src/dorkbox/network/connection/ListenerManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,22 +15,21 @@ */ package dorkbox.network.connection +import dorkbox.classUtil.ClassHelper +import dorkbox.classUtil.ClassHierarchy import dorkbox.collections.IdentityMap import dorkbox.network.ipFilter.IpFilterRule import dorkbox.os.OS -import dorkbox.util.classes.ClassHelper -import dorkbox.util.classes.ClassHierarchy -import kotlinx.atomicfu.atomic -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import mu.KLogger import net.jodah.typetools.TypeResolver +import org.slf4j.Logger +import java.net.InetAddress +import java.util.concurrent.locks.* +import kotlin.concurrent.write /** * Manages all of the different connect/disconnect/etc listeners */ -internal class ListenerManager(private val logger: KLogger) { +internal class ListenerManager(private val logger: Logger, val eventDispatch: EventDispatcher) { companion object { /** * Specifies the load-factor for the IdentityMap used to manage keeping track of the number of connections + listeners @@ -42,13 +41,13 @@ internal class ListenerManager(private val logger: KLogg * * Neither of these are useful in resolving exception handling from a users perspective, and only clutter the stacktrace. */ - fun cleanStackTrace(throwable: Throwable, adjustedStartOfStack: Int = 0) { + fun Throwable.cleanStackTrace(adjustedStartOfStack: Int = 0): Throwable { // we never care about coroutine stacks, so filter then to start with. - val origStackTrace = throwable.stackTrace + val origStackTrace = this.stackTrace val size = origStackTrace.size if (size == 0) { - return + return this } val stackTrace = origStackTrace.filterNot { @@ -85,15 +84,17 @@ internal class ListenerManager(private val logger: KLogg if (newEndIndex > 0) { if (savedFirstStack != null) { // we want to save the FIRST stack frame also, maybe - throwable.stackTrace = savedFirstStack + stackTrace.copyOfRange(newStartIndex, newEndIndex) + this.stackTrace = savedFirstStack + stackTrace.copyOfRange(newStartIndex, newEndIndex) } else { - throwable.stackTrace = stackTrace.copyOfRange(newStartIndex, newEndIndex) + this.stackTrace = stackTrace.copyOfRange(newStartIndex, newEndIndex) } } else { // keep just one, since it's a stack frame INSIDE our network library, and we need that! - throwable.stackTrace = stackTrace.copyOfRange(0, 1) + this.stackTrace = stackTrace.copyOfRange(0, 1) } + + return this } /** @@ -101,9 +102,9 @@ internal class ListenerManager(private val logger: KLogg * * Neither of these are useful in resolving exception handling from a users perspective, and only clutter the stacktrace. */ - fun cleanStackTraceInternal(throwable: Throwable) { + fun Throwable.cleanStackTraceInternal() { // NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace - val stackTrace = throwable.stackTrace + val stackTrace = this.stackTrace val size = stackTrace.size if (size == 0) { @@ -114,7 +115,7 @@ internal class ListenerManager(private val logger: KLogg val firstDorkboxIndex = stackTrace.indexOfFirst { it.className.startsWith("dorkbox.network.") } val lastDorkboxIndex = stackTrace.indexOfLast { it.className.startsWith("dorkbox.network.") } - throwable.stackTrace = stackTrace.filterIndexed { index, element -> + this.stackTrace = stackTrace.filterIndexed { index, element -> val stackName = element.className if (index <= firstDorkboxIndex && index >= lastDorkboxIndex) { false @@ -131,54 +132,72 @@ internal class ListenerManager(private val logger: KLogg * * We only want the error message, because we do something based on it (and the full stack trace is meaningless) */ - fun cleanAllStackTrace(throwable: Throwable) { - val stackTrace = throwable.stackTrace + fun Throwable.cleanAllStackTrace(): Throwable{ + val stackTrace = this.stackTrace val size = stackTrace.size if (size == 0) { - return + return this } // throw everything out - throwable.stackTrace = stackTrace.copyOfRange(0, 1) + this.stackTrace = stackTrace.copyOfRange(0, 1) + return this } - } - // initialize a emtpy arrays - private val onConnectFilterList = atomic(Array<(CONNECTION.() -> Boolean)>(0) { { true } }) - private val onConnectFilterMutex = Mutex() + internal inline fun add(thing: T, array: Array): Array { + val currentLength: Int = array.size + + // add the new subscription to the END of the array + @Suppress("UNCHECKED_CAST") + val newMessageArray = array.copyOf(currentLength + 1) as Array + newMessageArray[currentLength] = thing + + return newMessageArray + } - private val onInitList = atomic(Array Unit)>(0) { { } }) - private val onInitMutex = Mutex() + internal inline fun remove(thing: T, array: Array): Array { + // remove the subscription form the array + // THIS IS IDENTITY CHECKS, NOT EQUALITY + return array.filter { it !== thing }.toTypedArray() + } + } - private val onConnectList = atomic(Array Unit)>(0) { { } }) - private val onConnectMutex = Mutex() + // initialize emtpy arrays + @Volatile + private var onConnectFilterList = Array<((InetAddress, String) -> Boolean)>(0) { { _, _ -> true } } + private val onConnectFilterLock = ReentrantReadWriteLock() - private val onDisconnectList = atomic(Array Unit>(0) { { } }) - private val onDisconnectMutex = Mutex() + @Volatile + private var onConnectBufferedMessageFilterList = Array<((InetAddress?, String) -> Boolean)>(0) { { _, _ -> true } } + private val onConnectBufferedMessageFilterLock = ReentrantReadWriteLock() - private val onErrorList = atomic(Array Unit>(0) { { } }) - private val onErrorMutex = Mutex() + @Volatile + private var onInitList = Array<(CONNECTION.() -> Unit)>(0) { { } } + private val onInitLock = ReentrantReadWriteLock() - private val onErrorGlobalList = atomic(Array Unit>(0) { { } }) - private val onErrorGlobalMutex = Mutex() + @Volatile + private var onConnectList = Array<(CONNECTION.() -> Unit)>(0) { { } } + private val onConnectLock = ReentrantReadWriteLock() - private val onMessageMap = atomic(IdentityMap, Array Unit>>(32, LOAD_FACTOR)) - private val onMessageMutex = Mutex() + @Volatile + private var onDisconnectList = Array Unit>(0) { { } } + private val onDisconnectLock = ReentrantReadWriteLock() - // used to keep a cache of class hierarchy for distributing messages - private val classHierarchyCache = ClassHierarchy(LOAD_FACTOR) + @Volatile + private var onErrorList = Array Unit>(0) { { } } + private val onErrorLock = ReentrantReadWriteLock() - private inline fun add(thing: T, array: Array): Array { - val currentLength: Int = array.size + @Volatile + private var onErrorGlobalList = Array Unit>(0) { { } } + private val onErrorGlobalLock = ReentrantReadWriteLock() - // add the new subscription to the array - @Suppress("UNCHECKED_CAST") - val newMessageArray = array.copyOf(currentLength + 1) as Array - newMessageArray[currentLength] = thing + @Volatile + private var onMessageMap = IdentityMap, Array Unit>>(32, LOAD_FACTOR) + private val onMessageLock = ReentrantReadWriteLock() - return newMessageArray - } + // used to keep a cache of class hierarchy for distributing messages + private val classHierarchyCache = ClassHierarchy(LOAD_FACTOR) /** * Adds an IP+subnet rule that defines if that IP+subnet is allowed or denied connectivity to this server. @@ -186,10 +205,10 @@ internal class ListenerManager(private val logger: KLogg * If there are no rules added, then all connections are allowed * If there are rules added, then a rule MUST be matched to be allowed */ - suspend fun filter(ipFilterRule: IpFilterRule) { - filter { - // IPC will not filter, so this is OK to coerce to not-null - ipFilterRule.matches(remoteAddress!!) + fun filter(ipFilterRule: IpFilterRule) { + filter { clientAddress, _ -> + // IPC will not filter + ipFilterRule.matches(clientAddress) } } @@ -198,18 +217,51 @@ internal class ListenerManager(private val logger: KLogg * Adds a function that will be called BEFORE a client/server "connects" with each other, and used to determine if a connection * should be allowed * + * By default, if there are no filter rules, then all connections are allowed to connect + * If there are filter rules - then ONLY connections for the filter that returns true are allowed to connect (all else are denied) + * * It is the responsibility of the custom filter to write the error, if there is one * * If the function returns TRUE, then the connection will continue to connect. * If the function returns FALSE, then the other end of the connection will * receive a connection error * - * For a server, this function will be called for ALL clients. + * + * If ANY filter rule that is applied returns true, then the connection is permitted + * + * This function will be called for **only** network clients (IPC client are excluded) + * + * @param function clientAddress: UDP connection address + * tagName: the connection tag name + */ + fun filter(function: (clientAddress: InetAddress, tagName: String) -> Boolean) { + onConnectFilterLock.write { + // we have to follow the single-writer principle! + onConnectFilterList = add(function, onConnectFilterList) + } + } + + /** + * Adds a function that will be called BEFORE a client/server "connects" with each other, and used to determine if buffered messages + * for a connection should be enabled + * + * By default, if there are no rules, then all connections will have buffered messages enabled + * If there are rules - then ONLY connections for the rule that returns true will have buffered messages enabled (all else are disabled) + * + * It is the responsibility of the custom filter to write the error, if there is one + * + * If the function returns TRUE, then the buffered messages for a connection are enabled. + * If the function returns FALSE, then the buffered messages for a connection is disabled. + * + * If ANY rule that is applied returns true, then the buffered messages for a connection are enabled + * + * @param function clientAddress: not-null when UDP connection, null when IPC connection + * tagName: the connection tag name */ - suspend fun filter(function: CONNECTION.() -> Boolean) { - onConnectFilterMutex.withLock { + fun enableBufferedMessages(function: (clientAddress: InetAddress?, tagName: String) -> Boolean) { + onConnectBufferedMessageFilterLock.write { // we have to follow the single-writer principle! - onConnectFilterList.lazySet(add(function, onConnectFilterList.value)) + onConnectBufferedMessageFilterList = add(function, onConnectBufferedMessageFilterList) } } @@ -219,10 +271,10 @@ internal class ListenerManager(private val logger: KLogg * * For a server, this function will be called for ALL client connections. */ - suspend fun onInit(function: suspend CONNECTION.() -> Unit) { - onInitMutex.withLock { + fun onInit(function: CONNECTION.() -> Unit) { + onInitLock.write { // we have to follow the single-writer principle! - onInitList.lazySet(add(function, onInitList.value)) + onInitList = add(function, onInitList) } } @@ -230,10 +282,10 @@ internal class ListenerManager(private val logger: KLogg * Adds a function that will be called when a client/server connection first establishes a connection with the remote end. * 'onInit()' callbacks will execute for both the client and server before `onConnect()` will execute will "connects" with each other */ - suspend fun onConnect(function: suspend CONNECTION.() -> Unit) { - onConnectMutex.withLock { + fun onConnect(function: CONNECTION.() -> Unit) { + onConnectLock.write { // we have to follow the single-writer principle! - onConnectList.lazySet(add(function, onConnectList.value)) + onConnectList = add(function, onConnectList) } } @@ -242,10 +294,10 @@ internal class ListenerManager(private val logger: KLogg * * Do not try to send messages! The connection will already be closed, resulting in an error if you attempt to do so. */ - suspend fun onDisconnect(function: suspend CONNECTION.() -> Unit) { - onDisconnectMutex.withLock { + fun onDisconnect(function: CONNECTION.() -> Unit) { + onDisconnectLock.write { // we have to follow the single-writer principle! - onDisconnectList.lazySet(add(function, onDisconnectList.value)) + onDisconnectList = add(function, onDisconnectList) } } @@ -254,10 +306,10 @@ internal class ListenerManager(private val logger: KLogg * * The error is also sent to an error log before this method is called. */ - suspend fun onError(function: CONNECTION.(Throwable) -> Unit) { - onErrorMutex.withLock { + fun onError(function: CONNECTION.(Throwable) -> Unit) { + onErrorLock.write { // we have to follow the single-writer principle! - onErrorList.lazySet(add(function, onErrorList.value)) + onErrorList = add(function, onErrorList) } } @@ -266,10 +318,10 @@ internal class ListenerManager(private val logger: KLogg * * The error is also sent to an error log before this method is called. */ - suspend fun onError(function: Throwable.() -> Unit) { - onErrorGlobalMutex.withLock { + fun onError(function: Throwable.() -> Unit) { + onErrorGlobalLock.write { // we have to follow the single-writer principle! - onErrorGlobalList.lazySet(add(function, onErrorGlobalList.value)) + onErrorGlobalList = add(function, onErrorGlobalList) } } @@ -278,8 +330,8 @@ internal class ListenerManager(private val logger: KLogg * * This method should not block for long periods as other network activity will not be processed until it returns. */ - suspend fun onMessage(function: suspend CONNECTION.(MESSAGE) -> Unit) { - onMessageMutex.withLock { + fun onMessage(function: CONNECTION.(MESSAGE) -> Unit) { + onMessageLock.write { // we have to follow the single-writer principle! // this is the connection generic parameter for the listener, works for lambda expressions as well @@ -300,27 +352,27 @@ internal class ListenerManager(private val logger: KLogg } if (success) { - // NOTE: https://github.com/Kotlin/kotlinx.atomicfu + // https://github.com/Kotlin/kotlinx.atomicfu // this is EXPLICITLY listed as a "Don't" via the documentation. The ****ONLY**** reason this is actually OK is because // we are following the "single-writer principle", so only ONE THREAD can modify this at a time. - val tempMap = onMessageMap.value + val tempMap = onMessageMap @Suppress("UNCHECKED_CAST") - val func = function as suspend (CONNECTION, Any) -> Unit + val func = function as (CONNECTION, Any) -> Unit - val newMessageArray: Array Unit> - val onMessageArray: Array Unit>? = tempMap.get(messageClass) + val newMessageArray: Array<(CONNECTION, Any) -> Unit> + val onMessageArray: Array<(CONNECTION, Any) -> Unit>? = tempMap[messageClass] if (onMessageArray != null) { newMessageArray = add(function, onMessageArray) } else { @Suppress("RemoveExplicitTypeArguments") - newMessageArray = Array Unit>(1) { { _, _ -> } } + newMessageArray = Array<(CONNECTION, Any) -> Unit>(1) { { _, _ -> } } newMessageArray[0] = func } - tempMap.put(messageClass, newMessageArray) - onMessageMap.lazySet(tempMap) + tempMap.put(messageClass!!, newMessageArray) + onMessageMap = tempMap } else { throw IllegalArgumentException("Unable to add incompatible types! Detected connection/message classes: $connectionClass, $messageClass") } @@ -332,23 +384,18 @@ internal class ListenerManager(private val logger: KLogg * * It is the responsibility of the custom filter to write the error, if there is one * - * @return true if the connection will be allowed to connect. False if we should terminate this connection + * This is run directly on the thread that calls it! + * + * @return true if the client address is allowed to connect. False if we should terminate this connection */ - fun notifyFilter(connection: CONNECTION): Boolean { - // remote address will NOT be null at this stage, but best to verify. - val remoteAddress = connection.remoteAddress - if (remoteAddress == null) { - logger.error("Connection ${connection.id}: Unable to attempt connection stages when no remote address is present") - return false - } - + fun notifyFilter(clientAddress: InetAddress, clientTagName: String): Boolean { // by default, there is a SINGLE rule that will always exist, and will always ACCEPT ALL connections. // This is so the array types can be setup (the compiler needs SOMETHING there) - val arrayOfIpFilterRules = onConnectFilterList.value + val list = onConnectFilterList // if there is a rule, a connection must match for it to connect - arrayOfIpFilterRules.forEach { - if (it.invoke(connection)) { + list.forEach { + if (it.invoke(clientAddress, clientTagName)) { return true } } @@ -356,70 +403,135 @@ internal class ListenerManager(private val logger: KLogg // default if nothing matches // NO RULES ADDED -> ACCEPT // RULES ADDED -> DENY - return arrayOfIpFilterRules.isEmpty() + return list.isEmpty() } /** - * Invoked when a connection is first initialized, but BEFORE it's connected to the remote address. + * Invoked just after a connection is created, but before it is connected. + * + * It is the responsibility of the custom filter to write the error, if there is one + * + * This is run directly on the thread that calls it! + * + * @return true if the connection will have buffered messages enabled. False if buffered messages for this connection should be disabled. */ - fun notifyInit(connection: CONNECTION) { - runBlocking { - onInitList.value.forEach { - try { - it(connection) - } catch (t: Throwable) { - // NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace - cleanStackTrace(t) - logger.error("Connection ${connection.id} error", t) - } + fun notifyEnableBufferedMessages(clientAddress: InetAddress?, clientTagName: String): Boolean { + // by default, there is a SINGLE rule that will always exist, and will always PERMIT buffered messages. + // This is so the array types can be setup (the compiler needs SOMETHING there) + val list = onConnectBufferedMessageFilterList + + // if there is a rule, a connection must match for it to enable buffered messages + list.forEach { + if (it.invoke(clientAddress, clientTagName)) { + return true } } + + // default if nothing matches + // NO RULES ADDED -> ALLOW Buffered Messages + // RULES ADDED -> DISABLE Buffered Messages + return list.isEmpty() } /** - * Invoked when a connection is connected to a remote address. + * Invoked when a connection is first initialized, but BEFORE it's connected to the remote address. + * + * NOTE: This is run directly on the thread that calls it! Things that happen in event are TIME-CRITICAL, and must happen before connect happens. + * Because of this guarantee, init is immediately executed where connect is on a separate thread */ - suspend fun notifyConnect(connection: CONNECTION) { - onConnectList.value.forEach { + fun notifyInit(connection: CONNECTION) { + val list = onInitList + list.forEach { try { it(connection) } catch (t: Throwable) { - // NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace - cleanStackTrace(t) + // when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace + t.cleanStackTrace() logger.error("Connection ${connection.id} error", t) } } } + /** + * Invoked when a connection is connected to a remote address. + * + * This is run on the EventDispatch! + */ + fun notifyConnect(connection: CONNECTION) { + val list = onConnectList + if (list.isNotEmpty()) { + connection.endPoint.eventDispatch.CONNECT.launch { + list.forEach { + try { + it(connection) + } catch (t: Throwable) { + // when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace + t.cleanStackTrace() + logger.error("Connection ${connection.id} error", t) + } + } + } + } + } + /** * Invoked when a connection is disconnected to a remote address. + * + * This is exclusively called from a connection, when that connection is closed! + * + * This is run on the EventDispatch! */ - suspend fun notifyDisconnect(connection: CONNECTION) { - onDisconnectList.value.forEach { - try { - it(connection) - } catch (t: Throwable) { - // NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace - cleanStackTrace(t) - logger.error("Connection ${connection.id} error", t) + fun notifyDisconnect(connection: Connection) { + connection.notifyDisconnect() + + @Suppress("UNCHECKED_CAST") + directNotifyDisconnect(connection as CONNECTION) + } + + /** + * This is invoked by either a GLOBAL listener manager, or for a SPECIFIC CONNECTION listener manager. + */ + fun directNotifyDisconnect(connection: CONNECTION) { + val list = onDisconnectList + if (list.isNotEmpty()) { + connection.endPoint.eventDispatch.CLOSE.launch { + list.forEach { + try { + it(connection) + } catch (t: Throwable) { + // when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace + t.cleanStackTrace() + logger.error("Connection ${connection.id} error", t) + } + } } } } + /** * Invoked when there is an error for a specific connection * * The error is also sent to an error log before notifying callbacks + * + * This is run on the EventDispatch! */ fun notifyError(connection: CONNECTION, exception: Throwable) { - onErrorList.value.forEach { - try { - it(connection, exception) - } catch (t: Throwable) { - // NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace - cleanStackTrace(t) - logger.error("Connection ${connection.id} error", t) + val list = onErrorList + if (list.isNotEmpty()) { + connection.endPoint.eventDispatch.ERROR.launch { + list.forEach { + try { + it(connection, exception) + } catch (t: Throwable) { + // when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace + t.cleanStackTrace() + logger.error("Connection ${connection.id} error", t) + } + } } + } else { + logger.error("Error with connection $connection", exception) } } @@ -428,15 +540,22 @@ internal class ListenerManager(private val logger: KLogg * * The error is also sent to an error log before notifying callbacks */ - val notifyError: (exception: Throwable) -> Unit = { exception -> - onErrorGlobalList.value.forEach { - try { - it(exception) - } catch (t: Throwable) { - // NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace - cleanStackTrace(t) - logger.error("Global error", t) + fun notifyError(exception: Throwable) { + val list = onErrorGlobalList + if (list.isNotEmpty()) { + eventDispatch.ERROR.launch { + list.forEach { + try { + it(exception) + } catch (t: Throwable) { + // when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace + t.cleanStackTrace() + logger.error("Global error", t) + } + } } + } else { + logger.error("Global error", exception) } } @@ -445,7 +564,7 @@ internal class ListenerManager(private val logger: KLogg * * @return true if there were listeners assigned for this message type */ - suspend fun notifyOnMessage(connection: CONNECTION, message: Any): Boolean { + fun notifyOnMessage(connection: CONNECTION, message: Any): Boolean { val messageClass: Class<*> = message.javaClass // have to save the types + hierarchy (note: duplicates are OK, since they will just be overwritten) @@ -465,10 +584,10 @@ internal class ListenerManager(private val logger: KLogg // cache the lookup // we don't care about race conditions, since the object hierarchy will be ALREADY established at this exact moment - val tempMap = onMessageMap.value + val tempMap = onMessageMap var hasListeners = false hierarchy.forEach { clazz -> - val onMessageArray: Array Unit>? = tempMap.get(clazz) + val onMessageArray: Array<(CONNECTION, Any) -> Unit>? = tempMap[clazz] if (onMessageArray != null) { hasListeners = true @@ -476,8 +595,6 @@ internal class ListenerManager(private val logger: KLogg try { func(connection, message) } catch (t: Throwable) { - cleanStackTrace(t) - logger.error("Connection ${connection.id} error", t) notifyError(connection, t) } } @@ -486,4 +603,37 @@ internal class ListenerManager(private val logger: KLogg return hasListeners } + + /** + * This will remove all listeners that have been registered! + */ + fun close() { + // we have to follow the single-writer principle! + logger.debug("Closing the listener manager") + + onConnectFilterLock.write { + onConnectFilterList = Array(0) { { _, _ -> true } } + } + onConnectBufferedMessageFilterLock.write { + onConnectBufferedMessageFilterList = Array(0) { { _, _ -> true } } + } + onInitLock.write { + onInitList = Array(0) { { } } + } + onConnectLock.write { + onConnectList = Array(0) { { } } + } + onDisconnectLock.write { + onDisconnectList = Array(0) { { } } + } + onErrorLock.write { + onErrorList = Array(0) { { } } + } + onErrorGlobalLock.write { + onErrorGlobalList = Array(0) { { } } + } + onMessageLock.write { + onMessageMap = IdentityMap(32, LOAD_FACTOR) + } + } } diff --git a/src/dorkbox/network/connection/Paired.kt b/src/dorkbox/network/connection/Paired.kt new file mode 100644 index 00000000..3888d3a5 --- /dev/null +++ b/src/dorkbox/network/connection/Paired.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection + +class Paired { + lateinit var connection: CONNECTION + lateinit var message: Any +} diff --git a/src/dorkbox/network/connection/SendSync.kt b/src/dorkbox/network/connection/SendSync.kt new file mode 100644 index 00000000..1a5e64fd --- /dev/null +++ b/src/dorkbox/network/connection/SendSync.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection + +import dorkbox.network.rmi.RmiUtils + +class SendSync { + var message: Any? = null + + // used to notify the remote endpoint that the message has been processed + var id: Int = 0 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SendSync) return false + + if (message != other.message) return false + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + var result = message?.hashCode() ?: 0 + result = 31 * result + id + return result + } + + override fun toString(): String { + return "SendSync ${RmiUtils.unpackUnsignedRight(id)} (message=$message)" + } +} diff --git a/src/dorkbox/network/connection/buffer/BufferManager.kt b/src/dorkbox/network/connection/buffer/BufferManager.kt new file mode 100644 index 00000000..b1c049c8 --- /dev/null +++ b/src/dorkbox/network/connection/buffer/BufferManager.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.buffer + +import dorkbox.bytes.ByteArrayWrapper +import dorkbox.collections.LockFreeHashMap +import dorkbox.hex.toHexString +import dorkbox.network.Configuration +import dorkbox.network.aeron.AeronDriver +import dorkbox.network.connection.Connection +import dorkbox.network.connection.EndPoint +import dorkbox.network.connection.ListenerManager +import dorkbox.util.Sys +import net.jodah.expiringmap.ExpirationPolicy +import net.jodah.expiringmap.ExpiringMap +import org.slf4j.LoggerFactory +import java.util.concurrent.* + +internal open class BufferManager( + config: Configuration, + listenerManager: ListenerManager, + aeronDriver: AeronDriver, + sessionTimeout: Long +) { + + companion object { + private val logger = LoggerFactory.getLogger(BufferManager::class.java.simpleName) + } + + private val sessions = LockFreeHashMap() + private val expiringSessions: ExpiringMap + + init { + require(sessionTimeout >= 60) { "The buffered connection timeout 'bufferedConnectionTimeoutSeconds' must be greater than 60 seconds!" } + + // ignore 0 + val check = TimeUnit.SECONDS.toNanos(sessionTimeout) + val lingerNs = aeronDriver.lingerNs() + val required = TimeUnit.SECONDS.toNanos(config.connectionCloseTimeoutInSeconds.toLong()) + require(check == 0L || check > required + lingerNs) { + "The session timeout (${Sys.getTimePretty(check)}) must be longer than the connection close timeout (${Sys.getTimePretty(required)}) + the aeron driver linger timeout (${Sys.getTimePretty(lingerNs)})!" + } + + // connections are extremely difficult to diagnose when the connection timeout is short + val timeUnit = if (EndPoint.DEBUG_CONNECTIONS) { TimeUnit.HOURS } else { TimeUnit.SECONDS } + + expiringSessions = ExpiringMap.builder() + .expiration(sessionTimeout, timeUnit) + .expirationPolicy(ExpirationPolicy.CREATED) + .expirationListener { publicKeyWrapped, sessionConnection -> + // this blocks until it fully runs (which is ok. this is fast) + logger.debug("Connection session expired for: ${publicKeyWrapped.bytes.toHexString()}") + + // this SESSION has expired, so we should call the onDisconnect for the underlying connection, in order to clean it up. + listenerManager.notifyDisconnect(sessionConnection.connection) + } + .build() + } + + /** + * this must be called when a new connection is created + * + * @return true if this is a new session, false if it is an existing session + */ + fun onConnect(connection: Connection): BufferedSession { + val publicKeyWrapped = ByteArrayWrapper.wrap(connection.uuid) + + return synchronized(sessions) { + // always check if we are expiring first... + val expiring = expiringSessions.remove(publicKeyWrapped) + if (expiring != null) { + expiring.connection = connection + expiring + } else { + val existing = sessions[publicKeyWrapped] + if (existing != null) { + // we must always set this session value!! + existing.connection = connection + existing + } else { + val newSession = BufferedSession(connection) + sessions[publicKeyWrapped] = newSession + + // we must always set this when the connection is created, and it must be inside the sync block! + newSession + } + } + } + } + + /** + * Always called when a connection is disconnected from the network + */ + fun onDisconnect(connection: Connection) { + try { + val publicKeyWrapped = ByteArrayWrapper.wrap(connection.uuid) + + synchronized(sessions) { + val sess = sessions.remove(publicKeyWrapped) + // we want to expire this session after XYZ time + expiringSessions[publicKeyWrapped] = sess + } + } + catch (e: Exception) { + logger.error("Unable to run session expire logic!", e) + } + } + + + fun close() { + synchronized(sessions) { + sessions.clear() + expiringSessions.clear() + } + } +} diff --git a/src/dorkbox/network/connection/buffer/BufferedMessages.kt b/src/dorkbox/network/connection/buffer/BufferedMessages.kt new file mode 100644 index 00000000..ae5462f6 --- /dev/null +++ b/src/dorkbox/network/connection/buffer/BufferedMessages.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.buffer + +class BufferedMessages { + var messages = arrayListOf() +} diff --git a/src/dorkbox/network/connection/buffer/BufferedSerializer.kt b/src/dorkbox/network/connection/buffer/BufferedSerializer.kt new file mode 100644 index 00000000..7211d3db --- /dev/null +++ b/src/dorkbox/network/connection/buffer/BufferedSerializer.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.buffer + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output + +internal class BufferedSerializer: Serializer() { + override fun write(kryo: Kryo, output: Output, messages: BufferedMessages) { + kryo.writeClassAndObject(output, messages.messages) + } + + override fun read(kryo: Kryo, input: Input, type: Class): BufferedMessages { + val messages = BufferedMessages() + messages.messages = kryo.readClassAndObject(input) as ArrayList + return messages + } +} diff --git a/src/dorkbox/network/connection/buffer/BufferedSession.kt b/src/dorkbox/network/connection/buffer/BufferedSession.kt new file mode 100644 index 00000000..34cfddf5 --- /dev/null +++ b/src/dorkbox/network/connection/buffer/BufferedSession.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.buffer + +import dorkbox.network.connection.Connection +import java.util.concurrent.* + +open class BufferedSession(@Volatile var connection: Connection) { + /** + * Only used when configured. Will re-send all missing messages to a connection when a connection re-connects. + */ + val pendingMessagesQueue: LinkedTransferQueue = LinkedTransferQueue() + + fun queueMessage(connection: Connection, message: Any, abortEarly: Boolean): Boolean { + if (this.connection != connection) { + connection.logger.trace("[{}] message received on old connection, resending", connection) + + // we received a message on an OLD connection (which is no longer connected ---- BUT we have a NEW connection that is connected) + // this can happen on RMI object that are old + val success = this.connection.send(message, abortEarly) + if (success) { + connection.logger.trace("[{}] successfully resent message", connection) + return true + } + } + + if (!connection.enableBufferedMessages) { + // nothing, since we emit logs during connection initialization that pending messages are DISABLED + return false + } + + if (!abortEarly) { + // this was a "normal" send (instead of the disconnect message). + pendingMessagesQueue.put(message) + connection.logger.trace("[{}] queueing message", connection) + } + else if (connection.endPoint.aeronDriver.internal.mustRestartDriverOnError) { + // the only way we get errors, is if the connection is bad OR if we are sending so fast that the connection cannot keep up. + + // don't restart/reconnect -- there was an internal network error + pendingMessagesQueue.put(message) + connection.logger.trace("[{}] queueing message", connection) + } + else if (!connection.isClosedWithTimeout()) { + // there was an issue - the connection should automatically reconnect + pendingMessagesQueue.put(message) + connection.logger.trace("[{}] queueing message", connection) + } + + return false + } +} diff --git a/src/dorkbox/network/connection/buffer/package-info.java b/src/dorkbox/network/connection/buffer/package-info.java new file mode 100644 index 00000000..a291b834 --- /dev/null +++ b/src/dorkbox/network/connection/buffer/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.buffer; diff --git a/src/dorkbox/network/connection/package-info.java b/src/dorkbox/network/connection/package-info.java new file mode 100644 index 00000000..c4f60da9 --- /dev/null +++ b/src/dorkbox/network/connection/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection; diff --git a/src/dorkbox/network/connection/streaming/AeronWriter.kt b/src/dorkbox/network/connection/streaming/AeronWriter.kt new file mode 100644 index 00000000..1c621244 --- /dev/null +++ b/src/dorkbox/network/connection/streaming/AeronWriter.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.streaming + +import dorkbox.network.serialization.AeronOutput +import kotlinx.atomicfu.atomic + +class AeronWriter(val size: Int): StreamingWriter, AeronOutput(size) { + private val written = atomic(0) + + override fun writeBytes(startPosition: Int, bytes: ByteArray) { + position = startPosition + writeBytes(bytes) + written.getAndAdd(bytes.size) + } + + override fun isFinished(): Boolean { + return written.value == size + } +} diff --git a/src/dorkbox/network/connection/streaming/FileWriter.kt b/src/dorkbox/network/connection/streaming/FileWriter.kt new file mode 100644 index 00000000..3b09a446 --- /dev/null +++ b/src/dorkbox/network/connection/streaming/FileWriter.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.streaming + +import kotlinx.atomicfu.atomic +import java.io.File +import java.io.FileOutputStream +import java.io.RandomAccessFile + +class FileWriter(val size: Int, val file: File) : StreamingWriter, RandomAccessFile(file, "rw") { + + private val written = atomic(0) + + init { + // reserve space on disk! + val saveSize = size.coerceAtMost(4096) + var bytes = ByteArray(saveSize) + this.write(bytes) + + if (saveSize < size) { + var remainingBytes = size - saveSize + + while (remainingBytes > 0) { + if (saveSize > remainingBytes) { + bytes = ByteArray(remainingBytes) + } + this.write(bytes) + remainingBytes = (remainingBytes - saveSize).coerceAtLeast(0) + } + } + } + + override fun writeBytes(startPosition: Int, bytes: ByteArray) { + // the OS will synchronize writes to disk + this.seek(startPosition.toLong()) + write(bytes) + written.addAndGet(bytes.size) + } + + override fun isFinished(): Boolean { + return written.value == size + } + + fun finishAndClose() { + fd.sync() + close() + } +} diff --git a/src/dorkbox/network/connection/streaming/StreamingControl.kt b/src/dorkbox/network/connection/streaming/StreamingControl.kt index 1b9d653b..59902a77 100644 --- a/src/dorkbox/network/connection/streaming/StreamingControl.kt +++ b/src/dorkbox/network/connection/streaming/StreamingControl.kt @@ -1,5 +1,23 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.streaming -data class StreamingControl(val state: StreamingState, val streamId: Long, - val totalSize: Long = 0L, - val isFile: Boolean = false, val fileName: String = ""): StreamingMessage +data class StreamingControl(val state: StreamingState, + val isFile: Boolean, + val streamId: Int, + val totalSize: Long = 0L + ): StreamingMessage diff --git a/src/dorkbox/network/connection/streaming/StreamingSerializer.kt b/src/dorkbox/network/connection/streaming/StreamingControlSerializer.kt similarity index 57% rename from src/dorkbox/network/connection/streaming/StreamingSerializer.kt rename to src/dorkbox/network/connection/streaming/StreamingControlSerializer.kt index 5f5dd426..29372c3f 100644 --- a/src/dorkbox/network/connection/streaming/StreamingSerializer.kt +++ b/src/dorkbox/network/connection/streaming/StreamingControlSerializer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package dorkbox.network.connection.streaming import com.esotericsoftware.kryo.Kryo @@ -20,41 +21,21 @@ import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output -class StreamingControlSerializer: Serializer() { +internal class StreamingControlSerializer: Serializer() { override fun write(kryo: Kryo, output: Output, data: StreamingControl) { output.writeByte(data.state.ordinal) - output.writeVarLong(data.streamId, true) - output.writeVarLong(data.totalSize, true) output.writeBoolean(data.isFile) - if (data.isFile) { - output.writeString(data.fileName) - } + output.writeVarInt(data.streamId, true) + output.writeVarLong(data.totalSize, true) } override fun read(kryo: Kryo, input: Input, type: Class): StreamingControl { val stateOrdinal = input.readByte().toInt() - val state = StreamingState.values().first { it.ordinal == stateOrdinal } - val streamId = input.readVarLong(true) - val totalSize = input.readVarLong(true) val isFile = input.readBoolean() - val fileName = if (isFile) { - input.readString() - } else { - "" - } - - return StreamingControl(state, streamId, totalSize, isFile, fileName) - } -} - -class StreamingDataSerializer: Serializer() { - override fun write(kryo: Kryo, output: Output, data: StreamingData) { - output.writeVarLong(data.streamId, true) - } - - override fun read(kryo: Kryo, input: Input, type: Class): StreamingData { - val streamId = input.readVarLong(true) + val state = StreamingState.entries.first { it.ordinal == stateOrdinal } + val streamId = input.readVarInt(true) + val totalSize = input.readVarLong(true) - return StreamingData(streamId) + return StreamingControl(state, isFile, streamId, totalSize) } } diff --git a/src/dorkbox/network/connection/streaming/StreamingData.kt b/src/dorkbox/network/connection/streaming/StreamingData.kt index 14045f8a..0ecdbb1c 100644 --- a/src/dorkbox/network/connection/streaming/StreamingData.kt +++ b/src/dorkbox/network/connection/streaming/StreamingData.kt @@ -1,9 +1,27 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.streaming -class StreamingData(val streamId: Long) : StreamingMessage { +import dorkbox.bytes.xxHash32 + +class StreamingData(val streamId: Int) : StreamingMessage { - // These are set just after we receive the message, and before we process it - @Transient var payload: ByteArray? = null + var payload: ByteArray? = null + var startPosition: Int = 0 override fun equals(other: Any?): Boolean { if (this === other) return true @@ -17,16 +35,19 @@ class StreamingData(val streamId: Long) : StreamingMessage { if (!payload.contentEquals(other.payload)) return false } else if (other.payload != null) return false + if (startPosition != other.startPosition) return false + return true } override fun hashCode(): Int { var result = streamId.hashCode() result = 31 * result + (payload?.contentHashCode() ?: 0) + result = 31 * result + (startPosition) return result } override fun toString(): String { - return "StreamingData(streamId=$streamId)" + return "StreamingData(streamId=$streamId position=${startPosition}, xxHash=${payload?.xxHash32()})" } } diff --git a/src/dorkbox/network/connection/streaming/StreamingDataSerializer.kt b/src/dorkbox/network/connection/streaming/StreamingDataSerializer.kt new file mode 100644 index 00000000..13ea73b2 --- /dev/null +++ b/src/dorkbox/network/connection/streaming/StreamingDataSerializer.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.streaming + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output + + +internal class StreamingDataSerializer: Serializer() { + override fun write(kryo: Kryo, output: Output, data: StreamingData) { + output.writeVarInt(data.streamId, true) + // we re-use this data when streaming data to the remote endpoint, so we don't write out the payload here, we do it in another place + } + + override fun read(kryo: Kryo, input: Input, type: Class): StreamingData { + val streamId = input.readVarInt(true) + val streamingData = StreamingData(streamId) + + // we want to read out the start-position AND payload. It is not written by the serializer, but by the streaming manager + val startPosition = input.readVarInt(true) + val payloadSize = input.readVarInt(true) + streamingData.startPosition = startPosition + streamingData.payload = input.readBytes(payloadSize) + return streamingData + } +} diff --git a/src/dorkbox/network/connection/streaming/StreamingManager.kt b/src/dorkbox/network/connection/streaming/StreamingManager.kt index 6aa3805d..35dde4a8 100644 --- a/src/dorkbox/network/connection/streaming/StreamingManager.kt +++ b/src/dorkbox/network/connection/streaming/StreamingManager.kt @@ -1,30 +1,55 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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. + */ + @file:Suppress("DuplicatedCode") package dorkbox.network.connection.streaming +import com.esotericsoftware.kryo.io.Input +import dorkbox.bytes.OptimizeUtilsByteArray import dorkbox.bytes.OptimizeUtilsByteBuf -import dorkbox.collections.LockFreeHashMap +import dorkbox.collections.LockFreeLongMap +import dorkbox.network.Configuration import dorkbox.network.connection.Connection +import dorkbox.network.connection.CryptoManagement import dorkbox.network.connection.EndPoint -import dorkbox.network.connection.ListenerManager +import dorkbox.network.connection.ListenerManager.Companion.cleanAllStackTrace +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace import dorkbox.network.serialization.AeronInput import dorkbox.network.serialization.AeronOutput -import dorkbox.network.serialization.KryoExtra +import dorkbox.network.serialization.KryoWriter +import dorkbox.os.OS +import dorkbox.util.Sys import io.aeron.Publication -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import mu.KLogger import org.agrona.MutableDirectBuffer -import java.security.SecureRandom +import org.agrona.concurrent.IdleStrategy +import org.agrona.concurrent.UnsafeBuffer +import org.slf4j.Logger +import java.io.File +import java.io.FileInputStream -internal class StreamingManager(private val logger: KLogger, private val actionDispatch: CoroutineScope) { - private val streamingDataTarget = LockFreeHashMap() - private val streamingDataInMemory = LockFreeHashMap() +internal class StreamingManager(private val logger: Logger, val config: Configuration) { companion object { - val random = SecureRandom() + private const val KILOBYTE = 1024 + private const val MEGABYTE = 1024 * KILOBYTE + private const val GIGABYTE = 1024 * MEGABYTE + private const val TERABYTE = 1024L * GIGABYTE - @Suppress("UNUSED_CHANGED_VALUE") + @Suppress("UNUSED_CHANGED_VALUE", "SameParameterValue") private fun writeVarInt(internalBuffer: MutableDirectBuffer, position: Int, value: Int, optimizePositive: Boolean): Int { var p = position var newValue = value @@ -64,85 +89,171 @@ internal class StreamingManager(private val logger: KLo } + private val streamingDataTarget = LockFreeLongMap() + private val streamingDataInMemory = LockFreeLongMap() + + + /** + * What is the max stream size that can exist in memory when deciding if data blocks are in memory or temp-file on disk + */ + private val maxStreamSizeInMemoryInBytes = config.maxStreamSizeInMemoryMB * MEGABYTE + + fun getFile(connection: CONNECTION, endPoint: EndPoint, messageStreamId: Int): File { + // NOTE: the stream session ID is a combination of the connection ID + random ID (on the receiving side), + // otherwise clients can abuse it and corrupt OTHER clients data!! + val streamId = (connection.id.toLong() shl 4) or messageStreamId.toLong() + + val output = streamingDataInMemory[streamId] + return if (output is FileWriter) { + streamingDataInMemory.remove(streamId) + output.file + } else { + // something SUPER wrong! + // more critical error sending the message. we shouldn't retry or anything. + val errorMessage = "Error while reading file output, stream $streamId was of the wrong type!" + + // either client or server. No other choices. We create an exception, because it's more useful! + throw endPoint.newException(errorMessage) + } + } + /** - * Reassemble/figure out the internal message pieces + * NOTE: MUST BE ON THE AERON THREAD! + * + * Reassemble/figure out the internal message pieces. Processed always on the same thread */ - fun processControlMessage(message: StreamingControl, endPoint: EndPoint, connection: CONNECTION) { - val streamId = message.streamId + fun processControlMessage( + message: StreamingControl, + endPoint: EndPoint, + connection: CONNECTION + ) { + // NOTE: the stream session ID is a combination of the connection ID + random ID (on the receiving side), + // otherwise clients can abuse it and corrupt OTHER clients data!! + val streamId = (connection.id.toLong() shl 4) or message.streamId.toLong() when (message.state) { StreamingState.START -> { - streamingDataTarget[streamId] = message - if (!message.isFile) { - streamingDataInMemory[streamId] = AeronOutput() + // message.totalSize > maxInMemory OR if we are a file, then write to a temp file INSTEAD + if (message.isFile || message.totalSize > maxStreamSizeInMemoryInBytes) { + var fileName = "${config.appId}_${streamId}_${connection.id}.tmp" + + var tempFileLocation = OS.TEMP_DIR.resolve(fileName) + while (tempFileLocation.canRead()) { + fileName = "${config.appId}_${streamId}_${connection.id}_${CryptoManagement.secureRandom.nextInt()}.tmp" + tempFileLocation = OS.TEMP_DIR.resolve(fileName) + } + tempFileLocation.deleteOnExit() + + val prettySize = Sys.getSizePretty(message.totalSize) + + if (endPoint.logger.isInfoEnabled) { + endPoint.logger.info("Saving $prettySize of streaming data [${streamId}] to: $tempFileLocation") + } + streamingDataInMemory[streamId] = FileWriter(message.totalSize.toInt(), tempFileLocation) + } else { + if (endPoint.logger.isTraceEnabled) { + endPoint.logger.trace("Saving streaming data [${streamId}] in memory") + } + // .toInt is safe because we know the total size is < than maxStreamSizeInMemoryInBytes + streamingDataInMemory[streamId] = AeronWriter(message.totalSize.toInt()) } + + // this must be last + streamingDataTarget[streamId] = message } + StreamingState.FINISHED -> { + // NOTE: cannot be on a coroutine before kryo usage! + + if (message.isFile) { + // we do not do anything with this file yet! The serializer has to return this instance! + val output = streamingDataInMemory[streamId] + + if (output is FileWriter) { + output.finishAndClose() + // we don't need to do anything else (no de-serialization into an object) because we are already our target object + return + } else { + // something SUPER wrong! + // more critical error sending the message. we shouldn't retry or anything. + val errorMessage = "Error while processing streaming content, stream $streamId was supposed to be a FileWriter." + + // either client or server. No other choices. We create an exception, because it's more useful! + throw endPoint.newException(errorMessage) + } + } + // get the data out and send messages! - if (!message.isFile) { - val output = streamingDataInMemory.remove(streamId) - if (output != null) { - val kryo: KryoExtra = endPoint.serialization.takeKryo() + val output = streamingDataInMemory.remove(streamId) + + val input = when (output) { + is AeronWriter -> { + // the position can be wrong, especially if there are multiple threads setting the data + output.setPosition(output.size) + AeronInput(output.internalBuffer) + } + is FileWriter -> { + // if we are too large to fit in memory while streaming, we store it on disk. + output.finishAndClose() + val fileInputStream = FileInputStream(output.file) + Input(fileInputStream) + } + else -> { + null + } + } + + val streamedMessage = if (input != null) { + val kryo = endPoint.serialization.takeRead() try { - val input = AeronInput(output.internalBuffer) - val streamedMessage = kryo.read(input) - - // NOTE: This MUST be on a new co-routine - actionDispatch.launch { - val listenerManager = endPoint.listenerManager - - try { - @Suppress("UNCHECKED_CAST") - var hasListeners = listenerManager.notifyOnMessage(connection, streamedMessage) - - // each connection registers, and is polled INDEPENDENTLY for messages. - hasListeners = hasListeners or connection.notifyOnMessage(streamedMessage) - - if (!hasListeners) { - logger.error("No message callbacks found for ${streamedMessage::class.java.name}") - } - } catch (e: Exception) { - logger.error("Error processing message ${streamedMessage::class.java.name}", e) - listenerManager.notifyError(connection, e) - } - } + kryo.read(connection, input) } catch (e: Exception) { // something SUPER wrong! // more critical error sending the message. we shouldn't retry or anything. - val errorMessage = "Error serializing message from received streaming content, stream $streamId" + val errorMessage = "Error deserializing message from received streaming content, stream $streamId" // either client or server. No other choices. We create an exception, because it's more useful! - val exception = endPoint.newException(errorMessage, e) - - // +2 because we do not want to see the stack for the abstract `newException` - // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is - // where we see who is calling "send()" - ListenerManager.cleanStackTrace(exception, 2) - throw exception + throw endPoint.newException(errorMessage, e) } finally { - endPoint.serialization.returnKryo(kryo) + endPoint.serialization.putRead(kryo) + if (output is FileWriter) { + val fileName = "${config.appId}_${streamId}_${connection.id}.tmp" + val tempFileLocation = OS.TEMP_DIR.resolve(fileName) + tempFileLocation.delete() + } } } else { - // something SUPER wrong! - // more critical error sending the message. we shouldn't retry or anything. - val errorMessage = "Error while receiving streaming content, stream $streamId not available." - - // either client or server. No other choices. We create an exception, because it's more useful! - val exception = endPoint.newException(errorMessage) + null + } - // +2 because we do not want to see the stack for the abstract `newException` - // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is - // where we see who is calling "send()" - ListenerManager.cleanStackTrace(exception, 2) - throw exception + if (streamedMessage == null) { + if (output is FileWriter) { + val fileName = "${config.appId}_${streamId}_${connection.id}.tmp" + val tempFileLocation = OS.TEMP_DIR.resolve(fileName) + tempFileLocation.delete() } - } else { - // we are a file, so process accordingly + // something SUPER wrong! + // more critical error sending the message. we shouldn't retry or anything. + val errorMessage = "Error while processing streaming content, stream $streamId was null." + + // either client or server. No other choices. We create an exception, because it's more useful! + throw endPoint.newException(errorMessage) } + + + // this can be a regular message or an RMI message. Redispatch! + endPoint.processMessageFromChannel(connection, streamedMessage) } StreamingState.FAILED -> { + val output = streamingDataInMemory.remove(streamId) + if (output is FileWriter) { + val fileName = "${config.appId}_${streamId}_${connection.id}.tmp" + val tempFileLocation = OS.TEMP_DIR.resolve(fileName) + tempFileLocation.delete() + } + // clear all state // something SUPER wrong! // more critical error sending the message. we shouldn't retry or anything. @@ -151,13 +262,19 @@ internal class StreamingManager(private val logger: KLo // either client or server. No other choices. We create an exception, because it's more useful! val exception = endPoint.newException(errorMessage) - // +2 because we do not want to see the stack for the abstract `newException` // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is // where we see who is calling "send()" - ListenerManager.cleanStackTrace(exception, 2) + exception.cleanStackTrace(3) throw exception } StreamingState.UNKNOWN -> { + val output = streamingDataInMemory.remove(streamId) + if (output is FileWriter) { + val fileName = "${config.appId}_${streamId}_${connection.id}.tmp" + val tempFileLocation = OS.TEMP_DIR.resolve(fileName) + tempFileLocation.delete() + } + // something SUPER wrong! // more critical error sending the message. we shouldn't retry or anything. val errorMessage = "Unknown failure while receiving streaming content for stream $streamId" @@ -165,29 +282,29 @@ internal class StreamingManager(private val logger: KLo // either client or server. No other choices. We create an exception, because it's more useful! val exception = endPoint.newException(errorMessage) - // +2 because we do not want to see the stack for the abstract `newException` // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is // where we see who is calling "send()" - ListenerManager.cleanStackTrace(exception, 2) + exception.cleanStackTrace(3) throw exception } } } /** - * NOTE: MUST BE ON THE AERON THREAD! + * NOTE: MUST BE ON THE AERON THREAD BECAUSE THIS MUST BE SINGLE THREADED!!! * * Reassemble/figure out the internal message pieces * - * NOTE sending a huge file can prevent other other network traffic from arriving until it's done! + * NOTE sending a huge file can cause other network traffic delays! */ - fun processDataMessage(message: StreamingData, endPoint: EndPoint) { + fun processDataMessage(message: StreamingData, endPoint: EndPoint, connection: CONNECTION) { // the receiving data will ALWAYS come sequentially, but there might be OTHER streaming data received meanwhile. - val streamId = message.streamId + // NOTE: the stream session ID is a combination of the connection ID + random ID (on the receiving side) + val streamId = (connection.id.toLong() shl 4) or message.streamId.toLong() - val controlMessage = streamingDataTarget[streamId] - if (controlMessage != null) { - streamingDataInMemory.getOrPut(streamId) { AeronOutput() }.writeBytes(message.payload!!) + val dataWriter = streamingDataInMemory[streamId] + if (dataWriter != null) { + dataWriter.writeBytes(message.startPosition, message.payload!!) } else { // something SUPER wrong! // more critical error sending the message. we shouldn't retry or anything. @@ -196,23 +313,25 @@ internal class StreamingManager(private val logger: KLo // either client or server. No other choices. We create an exception, because it's more useful! val exception = endPoint.newException(errorMessage) - // +2 because we do not want to see the stack for the abstract `newException` // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is // where we see who is calling "send()" - ListenerManager.cleanStackTrace(exception, 5) + exception.cleanStackTrace(3) throw exception } } private fun sendFailMessageAndThrow( e: Exception, - streamSessionId: Long, + streamSessionId: Int, publication: Publication, endPoint: EndPoint, - connection: CONNECTION + sendIdleStrategy: IdleStrategy, + connection: CONNECTION, + kryo: KryoWriter ) { - val failMessage = StreamingControl(StreamingState.FAILED, streamSessionId) - val failSent = endPoint.send(failMessage, publication, connection) + val failMessage = StreamingControl(StreamingState.FAILED, false, streamSessionId) + + val failSent = endPoint.writeUnsafe(failMessage, publication, sendIdleStrategy, connection, kryo) if (!failSent) { // something SUPER wrong! // more critical error sending the message. we shouldn't retry or anything. @@ -221,10 +340,9 @@ internal class StreamingManager(private val logger: KLo // either client or server. No other choices. We create an exception, because it's more useful! val exception = endPoint.newException(errorMessage) - // +2 because we do not want to see the stack for the abstract `newException` // +4 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is // where we see who is calling "send()" - ListenerManager.cleanStackTrace(exception, 6) + exception.cleanStackTrace(4) throw exception } else { // send it up! @@ -241,84 +359,103 @@ internal class StreamingManager(private val logger: KLo * We don't write max possible length per message, we write out MTU (payload) length (so aeron doesn't fragment the message). * The max possible length is WAY, WAY more than the max payload length. * - * @param internalBuffer this is the ORIGINAL object data that is to be "chunked" and sent across the wire - * @return true if ALL the message chunks were successfully sent by aeron, false otherwise. Exceptions are caught and rethrown! + * @param originalBuffer this is the ORIGINAL object data that is to be blocks sent across the wire + * + * @return true if ALL the message blocks were successfully sent by aeron, false otherwise. Exceptions are caught and rethrown! */ fun send( publication: Publication, - internalBuffer: MutableDirectBuffer, + originalBuffer: MutableDirectBuffer, + maxMessageSize: Int, objectSize: Int, endPoint: EndPoint, - connection: CONNECTION): Boolean { - + kryo: KryoWriter, + sendIdleStrategy: IdleStrategy, + connection: CONNECTION + ): Boolean { // NOTE: our max object size for IN-MEMORY messages is an INT. For file transfer it's a LONG (so everything here is cast to a long) var remainingPayload = objectSize var payloadSent = 0 - val streamSessionId = random.nextLong() + + // NOTE: the stream session ID is a combination of the connection ID + random ID (on the receiving side) + val streamSessionId = CryptoManagement.secureRandom.nextInt() // tell the other side how much data we are sending - val startMessage = StreamingControl(StreamingState.START, streamSessionId, objectSize.toLong()) - val startSent = endPoint.send(startMessage, publication, connection) + val startMessage = StreamingControl(StreamingState.START, false, streamSessionId, remainingPayload.toLong()) + + val startSent = endPoint.writeUnsafe(startMessage, publication, sendIdleStrategy, connection, kryo) if (!startSent) { // more critical error sending the message. we shouldn't retry or anything. - val errorMessage = "[${publication.sessionId()}] Error starting streaming content." + val errorMessage = "[${publication.sessionId()}] Error starting streaming content (could not send data)." // either client or server. No other choices. We create an exception, because it's more useful! val exception = endPoint.newException(errorMessage) - // +2 because we do not want to see the stack for the abstract `newException` // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is // where we see who is calling "send()" - ListenerManager.cleanStackTrace(exception, 5) + exception.cleanStackTrace(3) throw exception } - val kryo: KryoExtra = endPoint.serialization.takeKryo() - - // we do the FIRST chunk super-weird, because of the way we copy data around (we inject headers, + // we do the FIRST block super-weird, because of the way we copy data around (we inject headers, // so the first message is SUPER tiny and is a COPY, the rest are no-copy. - // This is REUSED to prevent garbage collection issues. - val chunkData = StreamingData(streamSessionId) - // payload size is for a PRODUCER, and not SUBSCRIBER, so we have to include this amount every time. - // MINOR fragmentation by aeron is OK, since that will greatly speed up data transfer rates! - // the maxPayloadLength MUST ABSOLUTELY be less that the max size + header! - var sizeOfPayload = publication.maxMessageLength() - 200 + var sizeOfBlockData = maxMessageSize val header: ByteArray val headerSize: Int try { - val objectBuffer = kryo.write(connection, chunkData) + // This is REUSED to prevent garbage collection issues. + val blockData = StreamingData(streamSessionId) + val objectBuffer = kryo.write(connection, blockData) headerSize = objectBuffer.position() header = ByteArray(headerSize) - // we have to account for the header + the MAX optimized int size - sizeOfPayload -= (headerSize + 5) + // we have to account for the header + the MAX optimized int size (position and data-length) + val dataSize = headerSize + 5 + 5 + sizeOfBlockData -= dataSize // this size might be a LITTLE too big, but that's ok, since we only make this specific buffer once. - val chunkBuffer = AeronOutput(headerSize + sizeOfPayload) + val blockBuffer = AeronOutput(dataSize) // copy out our header info objectBuffer.internalBuffer.getBytes(0, header, 0, headerSize) // write out our header - chunkBuffer.writeBytes(header) + blockBuffer.writeBytes(header) - // write out the payload size using optimized data structures. - val varIntSize = chunkBuffer.writeVarInt(sizeOfPayload, true) + // write out the start-position (of the payload). First start-position is always 0 + val positionIntSize = blockBuffer.writeVarInt(0, true) - // write out the payload. Our resulting data written out is the ACTUAL MTU of aeron. - internalBuffer.getBytes(0, chunkBuffer.internalBuffer, headerSize + varIntSize, sizeOfPayload) + // write out the payload size + val payloadIntSize = blockBuffer.writeVarInt(sizeOfBlockData, true) - remainingPayload -= sizeOfPayload - payloadSent += sizeOfPayload + // write out the payload. Our resulting data written out is the ACTUAL MTU of aeron. + originalBuffer.getBytes(0, blockBuffer.internalBuffer, headerSize + positionIntSize + payloadIntSize, sizeOfBlockData) + + remainingPayload -= sizeOfBlockData + payloadSent += sizeOfBlockData + + // we reuse/recycle objects, so the payload size is not EXACTLY what is specified + val reusedPayloadSize = headerSize + positionIntSize + payloadIntSize + sizeOfBlockData + + val success = endPoint.aeronDriver.send( + publication = publication, + internalBuffer = blockBuffer.internalBuffer, + bufferClaim = kryo.bufferClaim, + offset = 0, + objectSize = reusedPayloadSize, + sendIdleStrategy = sendIdleStrategy, + connection = connection, + abortEarly = false, + listenerManager = endPoint.listenerManager + ) - val success = endPoint.sendData(publication, chunkBuffer.internalBuffer, 0, headerSize + varIntSize + sizeOfPayload, connection) if (!success) { // something SUPER wrong! // more critical error sending the message. we shouldn't retry or anything. @@ -327,25 +464,22 @@ internal class StreamingManager(private val logger: KLo // either client or server. No other choices. We create an exception, because it's more useful! val exception = endPoint.newException(errorMessage) - // +2 because we do not want to see the stack for the abstract `newException` // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is // where we see who is calling "send()" - ListenerManager.cleanStackTrace(exception, 5) + exception.cleanStackTrace(3) throw exception } } catch (e: Exception) { - sendFailMessageAndThrow(e, streamSessionId, publication, endPoint, connection) + sendFailMessageAndThrow(e, streamSessionId, publication, endPoint, sendIdleStrategy, connection, kryo) return false // doesn't actually get here because exceptions are thrown, but this makes the IDE happy. - } finally { - endPoint.serialization.returnKryo(kryo) } - // now send the chunks as fast as possible. Aeron will have us back-off if we send too quickly + // now send the block as fast as possible. Aeron will have us back-off if we send too quickly while (remainingPayload > 0) { - val amountToSend = if (remainingPayload < sizeOfPayload) { + val amountToSend = if (remainingPayload < sizeOfBlockData) { remainingPayload } else { - sizeOfPayload + sizeOfBlockData } remainingPayload -= amountToSend @@ -358,23 +492,283 @@ internal class StreamingManager(private val logger: KLo // fortunately, the way that serialization works, we can safely ADD data to the tail and then appropriately read it off // on the receiving end without worry. +/// TODO: Compression/encryption?? + try { - val varIntSize = OptimizeUtilsByteBuf.intLength(sizeOfPayload, true) - val writeIndex = payloadSent - headerSize - varIntSize + val positionIntSize = OptimizeUtilsByteBuf.intLength(payloadSent, true) + val payloadIntSize = OptimizeUtilsByteBuf.intLength(amountToSend, true) + val writeIndex = payloadSent - headerSize - positionIntSize - payloadIntSize // write out our header data (this will OVERWRITE previous data!) - internalBuffer.putBytes(writeIndex, header) + originalBuffer.putBytes(writeIndex, header) + + // write out the payload start position + writeVarInt(originalBuffer, writeIndex + headerSize, payloadSent, true) + + // write out the payload size + writeVarInt(originalBuffer, writeIndex + headerSize + positionIntSize, amountToSend, true) + + // we reuse/recycle objects, so the payload size is not EXACTLY what is specified + val reusedPayloadSize = headerSize + payloadIntSize + positionIntSize + amountToSend + + // write out the payload + val success = endPoint.aeronDriver.send( + publication = publication, + internalBuffer = originalBuffer, + bufferClaim = kryo.bufferClaim, + offset = writeIndex, + objectSize = reusedPayloadSize, + sendIdleStrategy = sendIdleStrategy, + connection = connection, + abortEarly = false, + listenerManager = endPoint.listenerManager + ) + + if (!success) { + // critical errors have an exception. Normal "the connection is closed" do not. + return false + } + + payloadSent += amountToSend + } catch (e: Exception) { + val failMessage = StreamingControl(StreamingState.FAILED, false, streamSessionId) + + val failSent = endPoint.writeUnsafe(failMessage, publication, sendIdleStrategy, connection, kryo) + if (!failSent) { + // something SUPER wrong! + // more critical error sending the message. we shouldn't retry or anything. + val errorMessage = "[${publication.sessionId()}] Abnormal failure with exception while streaming content." + + // either client or server. No other choices. We create an exception, because it's more useful! + val exception = endPoint.newException(errorMessage, e) + exception.cleanAllStackTrace() + throw exception + } else { + // send it up! + throw e + } + } + } + + // send the last block of data + val finishedMessage = StreamingControl(StreamingState.FINISHED, false, streamSessionId, payloadSent.toLong()) + + return endPoint.writeUnsafe(finishedMessage, publication, sendIdleStrategy, connection, kryo) + } + + /** + * This is called ONLY when a message is too large to send across the network in a single message (large data messages should + * be split into smaller ones anyways!) + * + * NOTE: this **MUST** stay on the same co-routine that calls "send". This cannot be re-dispatched onto a different coroutine! + * + * We don't write max possible length per message, we write out MTU (payload) length (so aeron doesn't fragment the message). + * The max possible length is WAY, WAY more than the max payload length. + * + * @param streamSessionId the stream session ID is a combination of the connection ID + random ID (on the receiving side) + * + * @return true if ALL the message blocks were successfully sent by aeron, false otherwise. Exceptions are caught and rethrown! + */ + @Suppress("SameParameterValue") + fun sendFile( + file: File, + publication: Publication, + endPoint: EndPoint, + kryo: KryoWriter, + sendIdleStrategy: IdleStrategy, + connection: CONNECTION, + streamSessionId: Int + ): Boolean { + val maxMessageSize = connection.maxMessageSize.toLong() + val fileInputStream = file.inputStream() + + // if the message is a file, we xfer the file AS a file, and leave it as a temp file (with a file reference to it) on the remote endpoint + // the temp file will be unique. + + // NOTE: our max object size for IN-MEMORY messages is an INT. For file transfer it's a LONG (so everything here is cast to a long) + var remainingPayload = file.length() + var payloadSent = 0 + + // tell the other side how much data we are sending + val startMessage = StreamingControl(StreamingState.START, true, streamSessionId, remainingPayload) - // write out the payload size using optimized data structures. - writeVarInt(internalBuffer, writeIndex + headerSize, sizeOfPayload, true) + val startSent = endPoint.writeUnsafe(startMessage, publication, sendIdleStrategy, connection, kryo) + if (!startSent) { + fileInputStream.close() + + // more critical error sending the message. we shouldn't retry or anything. + val errorMessage = "[${publication.sessionId()}] Error starting streaming file." + + // either client or server. No other choices. We create an exception, because it's more useful! + val exception = endPoint.newException(errorMessage) + + // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is + // where we see who is calling "send()" + exception.cleanStackTrace(3) + throw exception + } + + + + // we do the FIRST block super-weird, because of the way we copy data around (we inject headers), + // so the first message is SUPER tiny and is a COPY, the rest are no-copy. + + // payload size is for a PRODUCER, and not SUBSCRIBER, so we have to include this amount every time. + + // we don't know which is larger, the max message size or the file size! + var sizeOfBlockData = maxMessageSize.coerceAtMost(remainingPayload).toInt() + + val headerSize: Int + + val buffer: ByteArray + val blockBuffer: UnsafeBuffer + + try { + // This is REUSED to prevent garbage collection issues. + val blockData = StreamingData(streamSessionId) + val objectBuffer = kryo.write(connection, blockData) + headerSize = objectBuffer.position() + + // we have to account for the header + the MAX optimized int size (position and data-length) + val dataSize = headerSize + 5 + 5 + sizeOfBlockData -= dataSize + + // this size might be a LITTLE too big, but that's ok, since we only make this specific buffer once. + buffer = ByteArray(sizeOfBlockData + dataSize) + blockBuffer = UnsafeBuffer(buffer) + + // copy out our header info (this skips the header object) + objectBuffer.internalBuffer.getBytes(0, buffer, 0, headerSize) + + // write out the start-position (of the payload). First start-position is always 0 + val positionIntSize = OptimizeUtilsByteArray.writeInt(buffer, 0, true, headerSize) + + // write out the payload size + val payloadIntSize = OptimizeUtilsByteArray.writeInt(buffer, sizeOfBlockData, true, headerSize + positionIntSize) + + // write out the payload. Our resulting data written out is the ACTUAL MTU of aeron. + val readBytes = fileInputStream.read(buffer, headerSize + positionIntSize + payloadIntSize, sizeOfBlockData) + if (readBytes != sizeOfBlockData) { + // something SUPER wrong! + // more critical error sending the message. we shouldn't retry or anything. + val errorMessage = "[${publication.sessionId()}] Abnormal failure while streaming file (read bytes was wrong! ${readBytes} - ${sizeOfBlockData}." + + // either client or server. No other choices. We create an exception, because it's more useful! + val exception = endPoint.newException(errorMessage) + + // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is + // where we see who is calling "send()" + exception.cleanStackTrace(3) + throw exception + } + + remainingPayload -= sizeOfBlockData + payloadSent += sizeOfBlockData + + // we reuse/recycle objects, so the payload size is not EXACTLY what is specified + val reusedPayloadSize = headerSize + positionIntSize + payloadIntSize + sizeOfBlockData + + val success = endPoint.aeronDriver.send( + publication = publication, + internalBuffer = blockBuffer, + bufferClaim = kryo.bufferClaim, + offset = 0, + objectSize = reusedPayloadSize, + sendIdleStrategy = sendIdleStrategy, + connection = connection, + abortEarly = false, + listenerManager = endPoint.listenerManager + ) + + if (!success) { + // something SUPER wrong! + // more critical error sending the message. we shouldn't retry or anything. + val errorMessage = "[${publication.sessionId()}] Abnormal failure while streaming file." + + // either client or server. No other choices. We create an exception, because it's more useful! + val exception = endPoint.newException(errorMessage) + + // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is + // where we see who is calling "send()" + exception.cleanStackTrace(3) + throw exception + } + } catch (e: Exception) { + fileInputStream.close() + + sendFailMessageAndThrow(e, streamSessionId, publication, endPoint, sendIdleStrategy, connection, kryo) + return false // doesn't actually get here because exceptions are thrown, but this makes the IDE happy. + } + + + val aeronDriver = endPoint.aeronDriver + val listenerManager = endPoint.listenerManager + + // now send the block as fast as possible. Aeron will have us back-off if we send too quickly + while (remainingPayload > 0) { + val amountToSend = if (remainingPayload < sizeOfBlockData) { + remainingPayload.toInt() + } else { + sizeOfBlockData + } + + remainingPayload -= amountToSend + + + // to properly do this, we have to be careful with the underlying protocol, in order to avoid copying the buffer multiple times. + // the data that will be sent is object data + buffer data. We are sending the SAME parent buffer, just at different spots and + // with different headers -- so we don't copy out the data repeatedly + + // fortunately, the way that serialization works, we can safely ADD data to the tail and then appropriately read it off + // on the receiving end without worry. + +/// TODO: Compression/encryption?? + + try { + // write out the payload start position + val positionIntSize = OptimizeUtilsByteArray.writeInt(buffer, payloadSent, true, headerSize) + // write out the payload size + val payloadIntSize = OptimizeUtilsByteArray.writeInt(buffer, amountToSend, true, headerSize + positionIntSize) + + // write out the payload. Our resulting data written out is the ACTUAL MTU of aeron. + val readBytes = fileInputStream.read(buffer, headerSize + positionIntSize + payloadIntSize, amountToSend) + if (readBytes != amountToSend) { + // something SUPER wrong! + // more critical error sending the message. we shouldn't retry or anything. + val errorMessage = "[${publication.sessionId()}] Abnormal failure while streaming file (read bytes was wrong! ${readBytes} - ${amountToSend}." + + // either client or server. No other choices. We create an exception, because it's more useful! + val exception = endPoint.newException(errorMessage) + + // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is + // where we see who is calling "send()" + exception.cleanStackTrace(3) + throw exception + } + + // we reuse/recycle objects, so the payload size is not EXACTLY what is specified + val reusedPayloadSize = headerSize + positionIntSize + payloadIntSize + amountToSend // write out the payload - endPoint.sendData(publication, internalBuffer, writeIndex, headerSize + varIntSize + amountToSend, connection) + aeronDriver.send( + publication = publication, + internalBuffer = blockBuffer, + bufferClaim = kryo.bufferClaim, + offset = 0, // 0 because we are not reading the entire file at once + objectSize = reusedPayloadSize, + sendIdleStrategy = sendIdleStrategy, + connection = connection, + abortEarly = false, + listenerManager = listenerManager + ) payloadSent += amountToSend } catch (e: Exception) { - val failMessage = StreamingControl(StreamingState.FAILED, streamSessionId) - val failSent = endPoint.send(failMessage, publication, connection) + fileInputStream.close() + + val failMessage = StreamingControl(StreamingState.FAILED, false, streamSessionId) + + val failSent = endPoint.writeUnsafe(failMessage, publication, sendIdleStrategy, connection, kryo) if (!failSent) { // something SUPER wrong! // more critical error sending the message. we shouldn't retry or anything. @@ -383,10 +777,9 @@ internal class StreamingManager(private val logger: KLo // either client or server. No other choices. We create an exception, because it's more useful! val exception = endPoint.newException(errorMessage) - // +2 because we do not want to see the stack for the abstract `newException` // +3 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is // where we see who is calling "send()" - ListenerManager.cleanStackTrace(exception, 5) + exception.cleanStackTrace(3) throw exception } else { // send it up! @@ -395,8 +788,11 @@ internal class StreamingManager(private val logger: KLo } } - // send the last chunk of data - val finishedMessage = StreamingControl(StreamingState.FINISHED, streamSessionId, payloadSent.toLong()) - return endPoint.send(finishedMessage, publication, connection) + fileInputStream.close() + + // send the last block of data + val finishedMessage = StreamingControl(StreamingState.FINISHED, true, streamSessionId, payloadSent.toLong()) + + return endPoint.writeUnsafe(finishedMessage, publication, sendIdleStrategy, connection, kryo) } } diff --git a/src/dorkbox/network/connection/streaming/StreamingWriter.kt b/src/dorkbox/network/connection/streaming/StreamingWriter.kt new file mode 100644 index 00000000..48a0e5f3 --- /dev/null +++ b/src/dorkbox/network/connection/streaming/StreamingWriter.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.streaming + +interface StreamingWriter { + fun writeBytes(startPosition: Int, bytes: ByteArray) + fun isFinished(): Boolean +} diff --git a/src/dorkbox/network/connection/streaming/package-info.java b/src/dorkbox/network/connection/streaming/package-info.java new file mode 100644 index 00000000..90ba8c85 --- /dev/null +++ b/src/dorkbox/network/connection/streaming/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connection.streaming; diff --git a/src/dorkbox/network/connectionType/package-info.java b/src/dorkbox/network/connectionType/package-info.java new file mode 100644 index 00000000..299ca4b7 --- /dev/null +++ b/src/dorkbox/network/connectionType/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.connectionType; diff --git a/src/dorkbox/network/coroutines/SuspendFunctionTrampoline.java b/src/dorkbox/network/coroutines/SuspendFunctionTrampoline.java deleted file mode 100644 index f26ca945..00000000 --- a/src/dorkbox/network/coroutines/SuspendFunctionTrampoline.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 dorkbox, llc - * - * 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 dorkbox.network.coroutines; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import kotlin.coroutines.Continuation; -import kotlin.jvm.functions.Function1; - -/** - * Class to access suspending invocation of methods from kotlin... - * - * ULTIMATELY, this is all java bytecode, and the bytecode signature here matches what kotlin expects. The generics type information is - * discarded at compile time. - */ -public -class SuspendFunctionTrampoline { - - /** - * trampoline so we can access suspend functions correctly using reflection - */ - @SuppressWarnings("unchecked") - @Nullable - public static - Object invoke(@NotNull final Continuation continuation, @NotNull final Object suspendFunction) throws Throwable { - Function1, ?> suspendFunction1 = (Function1, ?>) suspendFunction; - return suspendFunction1.invoke((Continuation) continuation); - } -} diff --git a/src/dorkbox/network/exceptions/AllocationException.kt b/src/dorkbox/network/exceptions/AllocationException.kt index 585e9674..31f45169 100644 --- a/src/dorkbox/network/exceptions/AllocationException.kt +++ b/src/dorkbox/network/exceptions/AllocationException.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,6 @@ package dorkbox.network.exceptions /** - * A session/stream could not be allocated. + * A session/stream/resource could not be allocated. */ class AllocationException(message: String) : ServerException(message) diff --git a/src/dorkbox/network/exceptions/ClientHandshakeException.kt b/src/dorkbox/network/exceptions/ClientHandshakeException.kt new file mode 100644 index 00000000..5ddefdec --- /dev/null +++ b/src/dorkbox/network/exceptions/ClientHandshakeException.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.exceptions + +/** + * The type of exceptions raised by the client handshake. + */ +open class ClientHandshakeException : ClientException { + /** + * Create an exception. + * + * @param message The message + */ + constructor(message: String) : super(message) + + /** + * Create an exception. + * + * @param cause The cause + */ + constructor(cause: Throwable) : super(cause) + + /** + * Create an exception. + * + * @param message The message + * @param cause The cause + */ + constructor(message: String, cause: Throwable?) : super(message, cause) +} diff --git a/src/dorkbox/network/exceptions/MessageDispatchException.kt b/src/dorkbox/network/exceptions/MessageDispatchException.kt new file mode 100644 index 00000000..43a94d6e --- /dev/null +++ b/src/dorkbox/network/exceptions/MessageDispatchException.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.exceptions + +/** + * The type of exceptions raised for errors when dispatching messages + */ +open class MessageDispatchException : Exception { + /** + * Create an exception. + * + * @param message The message + */ + constructor(message: String) : super(message) + + /** + * Create an exception. + * + * @param cause The cause + */ + constructor(cause: Throwable) : super(cause) + + /** + * Create an exception. + * + * @param message The message + * @param cause The cause + */ + constructor(message: String, cause: Throwable?) : super(message, cause) +} diff --git a/src/dorkbox/network/exceptions/PingException.kt b/src/dorkbox/network/exceptions/PingException.kt new file mode 100644 index 00000000..fdc36509 --- /dev/null +++ b/src/dorkbox/network/exceptions/PingException.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.exceptions + +/** + * The type of exceptions raised for ping errors + */ +open class PingException : Exception { + /** + * Create an exception. + * + * @param message The message + */ + constructor(message: String) : super(message) + + /** + * Create an exception. + * + * @param cause The cause + */ + constructor(cause: Throwable) : super(cause) + + /** + * Create an exception. + * + * @param message The message + * @param cause The cause + */ + constructor(message: String, cause: Throwable?) : super(message, cause) +} diff --git a/src/dorkbox/network/exceptions/RMIException.kt b/src/dorkbox/network/exceptions/RMIException.kt new file mode 100644 index 00000000..7fa82554 --- /dev/null +++ b/src/dorkbox/network/exceptions/RMIException.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.exceptions + +/** + * The type of exceptions raised for RMI errors + */ +open class RMIException : Exception { + /** + * Create an exception. + * + * @param message The message + */ + constructor(message: String) : super(message) + + /** + * Create an exception. + * + * @param cause The cause + */ + constructor(cause: Throwable) : super(cause) + + /** + * Create an exception. + * + * @param message The message + * @param cause The cause + */ + constructor(message: String, cause: Throwable?) : super(message, cause) +} diff --git a/src/dorkbox/network/exceptions/SendSyncException.kt b/src/dorkbox/network/exceptions/SendSyncException.kt new file mode 100644 index 00000000..69ccef85 --- /dev/null +++ b/src/dorkbox/network/exceptions/SendSyncException.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.exceptions + +/** + * The type of exceptions raised for send-sync errors + */ +open class SendSyncException : Exception { + /** + * Create an exception. + * + * @param message The message + */ + constructor(message: String) : super(message) + + /** + * Create an exception. + * + * @param cause The cause + */ + constructor(cause: Throwable) : super(cause) + + /** + * Create an exception. + * + * @param message The message + * @param cause The cause + */ + constructor(message: String, cause: Throwable?) : super(message, cause) +} diff --git a/src/dorkbox/network/exceptions/SerializationException.kt b/src/dorkbox/network/exceptions/SerializationException.kt new file mode 100644 index 00000000..e88e3428 --- /dev/null +++ b/src/dorkbox/network/exceptions/SerializationException.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.exceptions + +/** + * The type of exceptions raised for a serialization error. + */ +open class SerializationException : Exception { + /** + * Create an exception. + * + * @param message The message + */ + constructor(message: String) : super(message) + + /** + * Create an exception. + * + * @param cause The cause + */ + constructor(cause: Throwable) : super(cause) + + /** + * Create an exception. + * + * @param message The message + * @param cause The cause + */ + constructor(message: String, cause: Throwable?) : super(message, cause) +} diff --git a/src/dorkbox/network/exceptions/ServerHandshakeException.kt b/src/dorkbox/network/exceptions/ServerHandshakeException.kt new file mode 100644 index 00000000..718b1623 --- /dev/null +++ b/src/dorkbox/network/exceptions/ServerHandshakeException.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.exceptions + +/** + * The type of exceptions raised by the server handshake. + */ +open class ServerHandshakeException : ServerException { + /** + * Create an exception. + * + * @param message The message + */ + constructor(message: String) : super(message) + + /** + * Create an exception. + * + * @param cause The cause + */ + constructor(cause: Throwable) : super(cause) + + /** + * Create an exception. + * + * @param message The message + * @param cause The cause + */ + constructor(message: String, cause: Throwable?) : super(message, cause) +} diff --git a/src/dorkbox/network/exceptions/ServerTimedoutException.kt b/src/dorkbox/network/exceptions/ServerTimedoutException.kt new file mode 100644 index 00000000..ef290f46 --- /dev/null +++ b/src/dorkbox/network/exceptions/ServerTimedoutException.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.exceptions + +/** + * The type of exceptions raised by the server handshake when it times out. + */ +open class ServerTimedoutException : ServerException { + /** + * Create an exception. + * + * @param message The message + */ + constructor(message: String) : super(message) + + /** + * Create an exception. + * + * @param cause The cause + */ + constructor(cause: Throwable) : super(cause) + + /** + * Create an exception. + * + * @param message The message + * @param cause The cause + */ + constructor(message: String, cause: Throwable?) : super(message, cause) +} diff --git a/src/dorkbox/network/exceptions/StreamingException.kt b/src/dorkbox/network/exceptions/StreamingException.kt new file mode 100644 index 00000000..7261bffc --- /dev/null +++ b/src/dorkbox/network/exceptions/StreamingException.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.exceptions + +/** + * The type of exceptions raised for a streaming error. + */ +open class StreamingException : Exception { + /** + * Create an exception. + * + * @param message The message + */ + constructor(message: String) : super(message) + + /** + * Create an exception. + * + * @param cause The cause + */ + constructor(cause: Throwable) : super(cause) + + /** + * Create an exception. + * + * @param message The message + * @param cause The cause + */ + constructor(message: String, cause: Throwable?) : super(message, cause) +} diff --git a/src/dorkbox/network/exceptions/PortAllocationException.kt b/src/dorkbox/network/exceptions/TimeoutException.kt similarity index 80% rename from src/dorkbox/network/exceptions/PortAllocationException.kt rename to src/dorkbox/network/exceptions/TimeoutException.kt index 30faa898..97a6ea26 100644 --- a/src/dorkbox/network/exceptions/PortAllocationException.kt +++ b/src/dorkbox/network/exceptions/TimeoutException.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package dorkbox.network.exceptions -/** - * A port could not be allocated. - */ -class PortAllocationException(message: String) : ServerException(message) +class TimeoutException: Exception() { + +} diff --git a/src/dorkbox/network/exceptions/TransmitException.kt b/src/dorkbox/network/exceptions/TransmitException.kt new file mode 100644 index 00000000..5bbbd009 --- /dev/null +++ b/src/dorkbox/network/exceptions/TransmitException.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.exceptions + +/** + * The type of exceptions raised when transmitting data. + */ +open class TransmitException : Exception { + /** + * Create an exception. + * + * @param message The message + */ + constructor(message: String) : super(message) + + /** + * Create an exception. + * + * @param cause The cause + */ + constructor(cause: Throwable) : super(cause) + + /** + * Create an exception. + * + * @param message The message + * @param cause The cause + */ + constructor(message: String, cause: Throwable?) : super(message, cause) +} diff --git a/src/dorkbox/network/exceptions/package-info.java b/src/dorkbox/network/exceptions/package-info.java new file mode 100644 index 00000000..e1b44240 --- /dev/null +++ b/src/dorkbox/network/exceptions/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.exceptions; diff --git a/src/dorkbox/network/handshake/ClientConnectionDriver.kt b/src/dorkbox/network/handshake/ClientConnectionDriver.kt new file mode 100644 index 00000000..84c75be7 --- /dev/null +++ b/src/dorkbox/network/handshake/ClientConnectionDriver.kt @@ -0,0 +1,254 @@ +/* + * Copyright 2024 dorkbox, llc + * + * 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 dorkbox.network.handshake + +import dorkbox.network.aeron.AeronDriver +import dorkbox.network.aeron.AeronDriver.Companion.getLocalAddressString +import dorkbox.network.aeron.AeronDriver.Companion.uri +import dorkbox.network.aeron.controlEndpoint +import dorkbox.network.aeron.endpoint +import dorkbox.network.connection.EndPoint +import dorkbox.network.exceptions.ClientRetryException +import dorkbox.network.exceptions.ClientTimedOutException +import io.aeron.CommonContext +import kotlinx.atomicfu.AtomicBoolean +import java.net.Inet4Address +import java.net.InetAddress + + +/** + * Set up the subscription + publication channels to the server + * + * Note: this class is NOT closed the traditional way! It's pub/sub objects are used by the connection (which is where they are closed) + * + * @throws ClientRetryException if we need to retry to connect + * @throws ClientTimedOutException if we cannot connect to the server in the designated time + */ +internal class ClientConnectionDriver(val connectionInfo: PubSub) { + + companion object { + fun build( + shutdown: AtomicBoolean, + aeronDriver: AeronDriver, + handshakeTimeoutNs: Long, + handshakeConnection: ClientHandshakeDriver, + connectionInfo: ClientConnectionInfo, + port2Server: Int, // this is the port2 value from the server + tagName: String + ): ClientConnectionDriver { + val handshakePubSub = handshakeConnection.pubSub + val reliable = handshakePubSub.reliable + + // flipped because we are connecting to these! + val sessionIdPub = connectionInfo.sessionIdSub + val sessionIdSub = connectionInfo.sessionIdPub + val streamIdPub = connectionInfo.streamIdSub + val streamIdSub = connectionInfo.streamIdPub + + val isUsingIPC = handshakePubSub.isIpc + + val logInfo: String + + val pubSub: PubSub + + if (isUsingIPC) { + // Create a subscription at the given address and port, using the given stream ID. + logInfo = "CONNECTION-IPC" + + pubSub = buildIPC( + shutdown = shutdown, + aeronDriver = aeronDriver, + handshakeTimeoutNs = handshakeTimeoutNs, + sessionIdPub = sessionIdPub, + sessionIdSub = sessionIdSub, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + reliable = reliable, + tagName = tagName, + logInfo = logInfo + ) + } + else { + val remoteAddress = handshakePubSub.remoteAddress + val remoteAddressString = handshakePubSub.remoteAddressString + val portPub = handshakePubSub.portPub + val portSub = handshakePubSub.portSub + + logInfo = if (remoteAddress is Inet4Address) { + "CONNECTION-IPv4" + } else { + "CONNECTION-IPv6" + } + + pubSub = buildUDP( + shutdown = shutdown, + aeronDriver = aeronDriver, + handshakeTimeoutNs = handshakeTimeoutNs, + sessionIdPub = sessionIdPub, + sessionIdSub = sessionIdSub, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + remoteAddress = remoteAddress!!, + remoteAddressString = remoteAddressString, + portPub = portPub, + portSub = portSub, + port2Server = port2Server, + reliable = reliable, + tagName = tagName, + logInfo = logInfo + ) + } + + return ClientConnectionDriver(pubSub) + } + + @Throws(ClientTimedOutException::class) + private fun buildIPC( + shutdown: AtomicBoolean, + aeronDriver: AeronDriver, + handshakeTimeoutNs: Long, + sessionIdPub: Int, + sessionIdSub: Int, + streamIdPub: Int, + streamIdSub: Int, + reliable: Boolean, + tagName: String, + logInfo: String + ): PubSub { + // on close, the publication CAN linger (in case a client goes away, and then comes back) + // AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) + + // Create a publication at the given address and port, using the given stream ID. + val publicationUri = uri(CommonContext.IPC_MEDIA, sessionIdPub, reliable) + + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be long running or re-entrant with the client. + + + // can throw an exception! We catch it in the calling class + val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, true) + + // can throw an exception! We catch it in the calling class + // we actually have to wait for it to connect before we continue + aeronDriver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause -> + ClientTimedOutException("$logInfo publication cannot connect with server!", cause) + } + + + // Create a subscription at the given address and port, using the given stream ID. + val subscriptionUri = uri(CommonContext.IPC_MEDIA, sessionIdSub, reliable) + val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, true) + + + // wait for the REMOTE end to also connect to us! + aeronDriver.waitForConnection(shutdown, subscription, handshakeTimeoutNs, logInfo) { cause -> + ClientTimedOutException("$logInfo subscription cannot connect with server!", cause) + } + + + return PubSub( + pub = publication, + sub = subscription, + sessionIdPub = sessionIdPub, + sessionIdSub = sessionIdSub, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + reliable = reliable, + remoteAddress = null, + remoteAddressString = EndPoint.IPC_NAME, + portPub = 0, + portSub = 0, + tagName = tagName + ) + } + + @Throws(ClientTimedOutException::class) + private fun buildUDP( + shutdown: AtomicBoolean, + aeronDriver: AeronDriver, + handshakeTimeoutNs: Long, + sessionIdPub: Int, + sessionIdSub: Int, + streamIdPub: Int, + streamIdSub: Int, + remoteAddress: InetAddress, + remoteAddressString: String, + portPub: Int, + portSub: Int, + port2Server: Int, // this is the port2 value from the server + reliable: Boolean, + tagName: String, + logInfo: String, + ): PubSub { + val isRemoteIpv4 = remoteAddress is Inet4Address + + // on close, the publication CAN linger (in case a client goes away, and then comes back) + // AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) + + // Create a publication at the given address and port, using the given stream ID. + val publicationUri = uri(CommonContext.UDP_MEDIA, sessionIdPub, reliable) + .endpoint(isRemoteIpv4, remoteAddressString, portPub) + + + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be long running or re-entrant with the client. + + // can throw an exception! We catch it in the calling class + val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, false) + + // can throw an exception! We catch it in the calling class + // we actually have to wait for it to connect before we continue + aeronDriver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause -> + ClientTimedOutException("$logInfo publication cannot connect with server $remoteAddressString", cause) + } + + // this will cause us to listen on the interface that connects with the remote address, instead of ALL interfaces. + val localAddressString = getLocalAddressString(publication, isRemoteIpv4) + + + // A control endpoint for the subscriptions will cause a periodic service management "heartbeat" to be sent to the + // remote endpoint publication, which permits the remote publication to send us data, thereby getting us around NAT + val subscriptionUri = uri(CommonContext.UDP_MEDIA, sessionIdSub, reliable) + .endpoint(isRemoteIpv4, localAddressString, portSub) + .controlEndpoint(isRemoteIpv4, remoteAddressString, port2Server) + .controlMode(CommonContext.MDC_CONTROL_MODE_DYNAMIC) + + val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, false) + + + // wait for the REMOTE end to also connect to us! + aeronDriver.waitForConnection(shutdown, subscription, handshakeTimeoutNs, logInfo) { cause -> + ClientTimedOutException("$logInfo subscription cannot connect with server!", cause) + } + + return PubSub( + pub = publication, + sub = subscription, + sessionIdPub = sessionIdPub, + sessionIdSub = sessionIdSub, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + reliable = reliable, + remoteAddress = remoteAddress, + remoteAddressString = remoteAddressString, + portPub = portPub, + portSub = portSub, + tagName = tagName + ) + } + } +} diff --git a/src/dorkbox/network/handshake/ClientConnectionInfo.kt b/src/dorkbox/network/handshake/ClientConnectionInfo.kt index 1b104ea0..fe157748 100644 --- a/src/dorkbox/network/handshake/ClientConnectionInfo.kt +++ b/src/dorkbox/network/handshake/ClientConnectionInfo.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,16 @@ */ package dorkbox.network.handshake -internal class ClientConnectionInfo(val port: Int = 0, - val sessionId: Int, - val streamId: Int = 0, - val publicKey: ByteArray = ByteArray(0), - val kryoRegistrationDetails: ByteArray) { -} +import javax.crypto.spec.SecretKeySpec + +internal class ClientConnectionInfo( + val sessionIdPub: Int = 0, + val sessionIdSub: Int = 0, + val streamIdPub: Int, + val streamIdSub: Int = 0, + val publicKey: ByteArray = ByteArray(0), + val sessionTimeout: Long, + val bufferedMessages: Boolean, + val kryoRegistrationDetails: ByteArray, + val secretKey: SecretKeySpec +) diff --git a/src/dorkbox/network/handshake/ClientHandshake.kt b/src/dorkbox/network/handshake/ClientHandshake.kt index 2f8693ed..851db13d 100644 --- a/src/dorkbox/network/handshake/ClientHandshake.kt +++ b/src/dorkbox/network/handshake/ClientHandshake.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2024 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,33 +16,32 @@ package dorkbox.network.handshake import dorkbox.network.Client -import dorkbox.network.aeron.mediaDriver.MediaDriverClient import dorkbox.network.connection.Connection import dorkbox.network.connection.CryptoManagement -import dorkbox.network.connection.ListenerManager -import dorkbox.network.exceptions.ClientRejectedException -import dorkbox.network.exceptions.ClientTimedOutException -import dorkbox.network.exceptions.ServerException +import dorkbox.network.connection.EndPoint +import dorkbox.network.connection.ListenerManager.Companion.cleanAllStackTrace +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTraceInternal +import dorkbox.network.exceptions.* +import dorkbox.util.Sys import io.aeron.FragmentAssembler +import io.aeron.Image import io.aeron.logbuffer.FragmentHandler import io.aeron.logbuffer.Header -import mu.KLogger import org.agrona.DirectBuffer -import java.lang.Thread.sleep -import java.util.concurrent.* +import org.slf4j.Logger internal class ClientHandshake( - private val crypto: CryptoManagement, - private val endPoint: Client, - private val logger: KLogger + private val client: Client, + private val logger: Logger ) { // @Volatile is used BECAUSE suspension of coroutines can continue on a DIFFERENT thread. We want to make sure that thread visibility is // correct when this happens. There are no race-conditions to be wary of. + private val crypto = client.crypto private val handler: FragmentHandler - private val pollIdleStrategy = endPoint.config.pollIdleStrategy.cloneToNormal() + private val handshaker = client.handshaker // used to keep track and associate UDP/IPC handshakes between client/server @Volatile @@ -61,28 +60,54 @@ internal class ClientHandshake( private var failedException: Exception? = null init { - // now we have a bi-directional connection with the server on the handshake "socket". + // NOTE: subscriptions (ie: reading from buffers, etc) are not thread safe! Because it is ambiguous HOW EXACTLY they are unsafe, + // we exclusively read from the DirectBuffer on a single thread. + + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be: + // - long running + // - re-entrant with the client handler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header -> // this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe! val sessionId = header.sessionId() val streamId = header.streamId() - val aeronLogInfo = "$sessionId/$streamId" - val message = endPoint.readHandshakeMessage(buffer, offset, length, header, aeronLogInfo) + // note: this address will ALWAYS be an IP:PORT combo OR it will be aeron:ipc (if IPC, it will be a different handler!) + val remoteIpAndPort = (header.context() as Image).sourceIdentity() + + // split + val splitPoint = remoteIpAndPort.lastIndexOf(':') + val clientAddressString = remoteIpAndPort.substring(0, splitPoint) + + val logInfo = "$sessionId/$streamId:$clientAddressString" failedException = null needToRetry = false - // it must be a registration message - if (message !is HandshakeMessage) { - failedException = ClientRejectedException("[$aeronLogInfo] cancelled handshake for unrecognized message: $message") - return@FragmentAssembler - } + + // ugh, this is verbose -- but necessary + val message = try { + val msg = handshaker.readMessage(buffer, offset, length) + + // VALIDATE:: a Registration object is the only acceptable message during the connection phase + if (msg !is HandshakeMessage) { + throw ClientRejectedException("[$logInfo] Connection not allowed! unrecognized message: $msg") .apply { cleanAllStackTrace() } + } else if (logger.isTraceEnabled) { + logger.trace("[$logInfo] (${msg.connectKey}) received HS: $msg") + } + msg + } catch (e: Exception) { + client.listenerManager.notifyError(ClientHandshakeException("[$logInfo] Error de-serializing handshake message!!", e)) + null + } ?: return@FragmentAssembler + + // this is an error message if (message.state == HandshakeMessage.INVALID) { - val cause = ServerException(message.errorMessage ?: "Unknown").apply { stackTrace = stackTrace.copyOfRange(0, 1) } - failedException = ClientRejectedException("[$aeronLogInfo} - ${message.connectKey}] cancelled handshake", cause) + val cause = ServerException(message.errorMessage ?: "Unknown").apply { stackTrace = emptyArray() } + failedException = ClientRejectedException("[$logInfo}] (${message.connectKey}) cancelled handshake", cause) + .apply { cleanAllStackTrace() } return@FragmentAssembler } @@ -94,7 +119,7 @@ internal class ClientHandshake( } if (connectKey != message.connectKey) { - logger.error("[$aeronLogInfo - $connectKey] ignored handshake for ${message.connectKey} (Was for another client)") + logger.error("[$logInfo] ($connectKey) ignored handshake for ${message.connectKey} (Was for another client)") return@FragmentAssembler } @@ -110,28 +135,20 @@ internal class ClientHandshake( if (registrationData != null && serverPublicKeyBytes != null) { connectionHelloInfo = crypto.decrypt(registrationData, serverPublicKeyBytes) } else { - failedException = ClientRejectedException("[$aeronLogInfo} - ${message.connectKey}] canceled handshake for message without registration and/or public key info") + failedException = ClientRejectedException("[$logInfo}] (${message.connectKey}) canceled handshake for message without registration and/or public key info") + .apply { cleanAllStackTrace() } } } HandshakeMessage.HELLO_ACK_IPC -> { // The message was intended for this client. Try to parse it as one of the available message types. // this message is NOT-ENCRYPTED! - val cryptInput = crypto.cryptInput - - if (registrationData != null) { - cryptInput.buffer = registrationData - - val sessId = cryptInput.readInt() - val streamPubId = cryptInput.readInt() - val regDetailsSize = cryptInput.readInt() - val regDetails = cryptInput.readBytes(regDetailsSize) + val serverPublicKeyBytes = message.publicKey - // now read data off - connectionHelloInfo = ClientConnectionInfo(sessionId = sessId, - port = streamPubId, - kryoRegistrationDetails = regDetails) + if (registrationData != null && serverPublicKeyBytes != null) { + connectionHelloInfo = crypto.nocrypt(registrationData, serverPublicKeyBytes) } else { - failedException = ClientRejectedException("[$aeronLogInfo - ${message.connectKey}] canceled handshake for message without registration data") + failedException = ClientRejectedException("[$logInfo}] (${message.connectKey}) canceled handshake for message without registration and/or public key info") + .apply { cleanAllStackTrace() } } } HandshakeMessage.DONE_ACK -> { @@ -139,7 +156,8 @@ internal class ClientHandshake( } else -> { val stateString = HandshakeMessage.toStateString(message.state) - failedException = ClientRejectedException("[$aeronLogInfo - ${message.connectKey}] cancelled handshake for message that is $stateString") + failedException = ClientRejectedException("[$logInfo] (${message.connectKey}) cancelled handshake for message that is $stateString") + .apply { cleanAllStackTrace() } } } } @@ -149,76 +167,75 @@ internal class ClientHandshake( * Make sure that NON-ZERO is returned */ private fun getSafeConnectKey(): Long { - var key = endPoint.crypto.secureRandom.nextLong() + var key = CryptoManagement.secureRandom.nextLong() while (key == 0L) { - key = endPoint.crypto.secureRandom.nextLong() + key = CryptoManagement.secureRandom.nextLong() } return key } // called from the connect thread - fun hello(handshakeConnection: MediaDriverClient, connectionTimeoutSec: Int) : ClientConnectionInfo { - failedException = null - connectKey = getSafeConnectKey() - val publicKey = endPoint.storage.getPublicKey()!! - - val aeronLogInfo = "${handshakeConnection.remoteSessionId}/${handshakeConnection.streamId}" + // when exceptions are thrown, the handshake pub/sub will be closed + fun hello( + tagName: String, + endPoint: EndPoint, + handshakeConnection: ClientHandshakeDriver, + handshakeTimeoutNs: Long + ) : ClientConnectionInfo { + val pubSub = handshakeConnection.pubSub + + // is our pub still connected?? + if (!pubSub.pub.isConnected) { + throw ClientException("Handshake publication is not connected, and it is expected to be connected!") + } - // Send the one-time pad to the server. - val publication = handshakeConnection.publication - val subscription = handshakeConnection.subscription + // always make sure that we reset the state when we start (in the event of reconnects) + reset() + connectKey = getSafeConnectKey() try { - endPoint.writeHandshakeMessage(publication, aeronLogInfo, - HandshakeMessage.helloFromClient(connectKey, publicKey, - handshakeConnection.localSessionId, - handshakeConnection.subscriptionPort, - handshakeConnection.subscription.streamId())) + // Send the one-time pad to the server. + handshaker.writeMessage(pubSub.pub, handshakeConnection.details, + HandshakeMessage.helloFromClient( + connectKey = connectKey, + publicKey = client.storage.publicKey, + streamIdSub = pubSub.streamIdSub, + portSub = pubSub.portSub, + tagName = tagName + )) } catch (e: Exception) { - subscription.close() - publication.close() - - logger.error("[$aeronLogInfo] Handshake error!", e) - throw e + handshakeConnection.close(endPoint) + throw TransmitException("$handshakeConnection Handshake message error!", e) } // block until we receive the connection information from the server - var pollCount: Int - pollIdleStrategy.reset() - val timoutInNanos = TimeUnit.SECONDS.toNanos(connectionTimeoutSec.toLong()) + endPoint.aeronDriver.getLingerNs() val startTime = System.nanoTime() - while (System.nanoTime() - startTime < timoutInNanos) { + while (System.nanoTime() - startTime < handshakeTimeoutNs) { // NOTE: regarding fragment limit size. Repeated calls to '.poll' will reassemble a fragment. // `.poll(handler, 4)` == `.poll(handler, 2)` + `.poll(handler, 2)` - pollCount = subscription.poll(handler, 1) + pubSub.sub.poll(handler, 1) - if (failedException != null || connectionHelloInfo != null) { + if (endPoint.isShutdown() || failedException != null || connectionHelloInfo != null) { break } - // 0 means we idle. >0 means reset and don't idle (because there are likely more) - pollIdleStrategy.idle(pollCount) + Thread.sleep(100) } val failedEx = failedException if (failedEx != null) { - // no longer necessary to hold this connection open (if not a failure, we close the handshake after the DONE message) - subscription.close() - publication.close() + handshakeConnection.close(endPoint) - ListenerManager.cleanStackTraceInternal(failedEx) + failedEx.cleanStackTraceInternal() throw failedEx } if (connectionHelloInfo == null) { - // no longer necessary to hold this connection open (if not a failure, we close the handshake after the DONE message) - subscription.close() - publication.close() + handshakeConnection.close(endPoint) - val exception = ClientTimedOutException("[$aeronLogInfo] Waiting for registration response from server") - ListenerManager.cleanStackTraceInternal(exception) + val exception = ClientTimedOutException("$handshakeConnection Waiting for registration response from server for more than ${Sys.getTimePrettyFull(handshakeTimeoutNs)}") throw exception } @@ -226,39 +243,47 @@ internal class ClientHandshake( } // called from the connect thread - fun done(handshakeConnection: MediaDriverClient, connectionTimeoutSec: Int) { - val registrationMessage = HandshakeMessage.doneFromClient(connectKey, - handshakeConnection.subscriptionPort, - handshakeConnection.subscription.streamId()) - - val aeronLogInfo = "${handshakeConnection.remoteSessionId}/${handshakeConnection.streamId}" + // when exceptions are thrown, the handshake pub/sub will be closed + fun done( + endPoint: EndPoint, + handshakeConnection: ClientHandshakeDriver, + clientConnection: ClientConnectionDriver, + handshakeTimeoutNs: Long, + logInfo: String + ) { + val pubSub = clientConnection.connectionInfo + val handshakePubSub = handshakeConnection.pubSub + + // is our pub still connected?? + if (!pubSub.pub.isConnected) { + throw ClientException("Handshake publication is not connected, and it is expected to be connected!") + } // Send the done message to the server. try { - endPoint.writeHandshakeMessage(handshakeConnection.publication, aeronLogInfo, registrationMessage) + handshaker.writeMessage(handshakeConnection.pubSub.pub, logInfo, + HandshakeMessage.doneFromClient( + connectKey = connectKey, + sessionIdSub = handshakePubSub.sessionIdSub, + streamIdSub = handshakePubSub.streamIdSub + )) } catch (e: Exception) { - handshakeConnection.subscription.close() - handshakeConnection.publication.close() - throw e + handshakeConnection.close(endPoint) + throw TransmitException("$handshakeConnection Handshake message error!", e) } - // block until we receive the connection information from the server failedException = null - pollIdleStrategy.reset() - - var pollCount: Int - - val subscription = handshakeConnection.subscription + connectionDone = false - val timoutInNanos = TimeUnit.SECONDS.toNanos(connectionTimeoutSec.toLong()) + // block until we receive the connection information from the server var startTime = System.nanoTime() - while (System.nanoTime() - startTime < timoutInNanos) { + while (System.nanoTime() - startTime < handshakeTimeoutNs) { // NOTE: regarding fragment limit size. Repeated calls to '.poll' will reassemble a fragment. // `.poll(handler, 4)` == `.poll(handler, 2)` + `.poll(handler, 2)` - pollCount = subscription.poll(handler, 1) + handshakePubSub.sub.poll(handler, 1) - if (failedException != null || connectionDone) { + if (endPoint.isShutdown() || failedException != null || connectionDone) { break } @@ -269,20 +294,21 @@ internal class ClientHandshake( startTime = System.nanoTime() } - sleep(100L) - - // 0 means we idle. >0 means reset and don't idle (because there are likely more) - pollIdleStrategy.idle(pollCount) + Thread.sleep(100) } val failedEx = failedException if (failedEx != null) { + handshakeConnection.close(endPoint) + throw failedEx } if (!connectionDone) { - val exception = ClientTimedOutException("Waiting for registration response from server") - ListenerManager.cleanStackTraceInternal(exception) + // since this failed, close everything + handshakeConnection.close(endPoint) + + val exception = ClientTimedOutException("Timed out waiting for registration response from server: ${Sys.getTimePrettyFull(handshakeTimeoutNs)}") throw exception } } diff --git a/src/dorkbox/network/handshake/ClientHandshakeDriver.kt b/src/dorkbox/network/handshake/ClientHandshakeDriver.kt new file mode 100644 index 00000000..90327ec3 --- /dev/null +++ b/src/dorkbox/network/handshake/ClientHandshakeDriver.kt @@ -0,0 +1,402 @@ +/* + * Copyright 2024 dorkbox, llc + * + * 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 dorkbox.network.handshake + +import dorkbox.network.aeron.AeronDriver +import dorkbox.network.aeron.AeronDriver.Companion.getLocalAddressString +import dorkbox.network.aeron.AeronDriver.Companion.streamIdAllocator +import dorkbox.network.aeron.AeronDriver.Companion.uri +import dorkbox.network.aeron.AeronDriver.Companion.uriHandshake +import dorkbox.network.aeron.controlEndpoint +import dorkbox.network.aeron.endpoint +import dorkbox.network.connection.CryptoManagement +import dorkbox.network.connection.EndPoint +import dorkbox.network.connection.ListenerManager.Companion.cleanAllStackTrace +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTraceInternal +import dorkbox.network.exceptions.ClientException +import dorkbox.network.exceptions.ClientRetryException +import dorkbox.network.exceptions.ClientTimedOutException +import dorkbox.util.Sys +import io.aeron.CommonContext +import io.aeron.Subscription +import kotlinx.atomicfu.AtomicBoolean +import org.slf4j.Logger +import java.net.Inet4Address +import java.net.InetAddress +import java.util.* + + + +/** + * Set up the subscription + publication channels to the server + * + * @throws ClientRetryException if we need to retry to connect + * @throws ClientTimedOutException if we cannot connect to the server in the designated time + */ +internal class ClientHandshakeDriver( + val aeronDriver: AeronDriver, + val pubSub: PubSub, + private val logInfo: String, + val details: String +) { + companion object { + fun build( + endpoint: EndPoint<*>, + aeronDriver: AeronDriver, + autoChangeToIpc: Boolean, + remoteAddress: InetAddress?, + remoteAddressString: String, + remotePort1: Int, + remotePort2: Int, + clientListenPort: Int, + handshakeTimeoutNs: Long, + connectionTimoutInNs: Long, + reliable: Boolean, + tagName: String, + logger: Logger + ): ClientHandshakeDriver { + logger.trace("Starting client handshake") + + var isUsingIPC = false + + if (autoChangeToIpc) { + if (remoteAddress == null) { + logger.info("IPC enabled") + } else { + logger.warn("IPC for loopback enabled and aeron is already running. Auto-changing network connection from '$remoteAddressString' -> IPC") + } + isUsingIPC = true + } + + + var logInfo = "" + + var details = "" + + // this must be unique otherwise we CANNOT connect to the server! + val sessionIdPub = CryptoManagement.secureRandom.nextInt() + + // with IPC, the aeron driver MUST be shared, so having a UNIQUE sessionIdPub/Sub is unnecessary. +// sessionIdPub = sessionIdAllocator.allocate() +// sessionIdSub = sessionIdAllocator.allocate() + // streamIdPub is assigned by ipc/udp directly + var streamIdPub: Int + val streamIdSub = streamIdAllocator.allocate() // sub stream ID so the server can comm back to the client + + var pubSub: PubSub? = null + + val timeoutInfo = if (connectionTimoutInNs > 0L) { + "[Handshake: ${Sys.getTimePrettyFull(handshakeTimeoutNs)}, Max connection attempt: ${Sys.getTimePrettyFull(connectionTimoutInNs)}]" + } else { + "[Handshake: ${Sys.getTimePrettyFull(handshakeTimeoutNs)}, Max connection attempt: Unlimited]" + } + + val config = endpoint.config + val shutdown = endpoint.shutdown + + if (isUsingIPC) { + streamIdPub = config.ipcId + + logInfo = "HANDSHAKE-IPC" + details = logInfo + + + logger.info("Client connecting via IPC. $timeoutInfo") + + try { + pubSub = buildIPC( + shutdown = shutdown, + aeronDriver = aeronDriver, + handshakeTimeoutNs = handshakeTimeoutNs, + sessionIdPub = sessionIdPub, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + reliable = reliable, + tagName = tagName, + logInfo = logInfo + ) + } catch (exception: Exception) { + logger.error("Error initializing IPC connection", exception) + + // MAYBE the server doesn't have IPC enabled? If no, we need to connect via network instead + isUsingIPC = false + + // we will retry! + if (remoteAddress == null) { + // the exception will HARD KILL the client, make sure aeron driver is closed. + aeronDriver.close() + + // if we specified that we MUST use IPC, then we have to throw the exception, because there is no IPC + val clientException = ClientException("Unable to connect via IPC to server. No address specified so fallback is unavailable", exception) + clientException.cleanStackTraceInternal() + throw clientException + } + } + } + + if (!isUsingIPC) { + if (remoteAddress == null) { + val clientException = ClientException("Unable to connect via UDP to server. No address specified!") + clientException.cleanStackTraceInternal() + throw clientException + } + + logInfo = if (remoteAddress is Inet4Address) { + "HANDSHAKE-IPv4" + } else { + "HANDSHAKE-IPv6" + } + + streamIdPub = config.udpId + + + if (remoteAddress is Inet4Address) { + logger.info("Client connecting to IPv4 $remoteAddressString. $timeoutInfo") + } else { + logger.info("Client connecting to IPv6 $remoteAddressString. $timeoutInfo") + } + + pubSub = buildUDP( + shutdown = shutdown, + aeronDriver = aeronDriver, + handshakeTimeoutNs = handshakeTimeoutNs, + remoteAddress = remoteAddress, + remoteAddressString = remoteAddressString, + portPub = remotePort1, + portSub = clientListenPort, + port2Server = remotePort2, + sessionIdPub = sessionIdPub, + streamIdPub = streamIdPub, + reliable = reliable, + streamIdSub = streamIdSub, + tagName = tagName, + logInfo = logInfo + ) + + + // we have to figure out what our sub port info is, otherwise the server cannot connect back! + val subscriptionAddress = try { + getLocalAddressString(pubSub.sub) + } catch (e: Exception) { + throw ClientRetryException("$logInfo subscription is not properly created!", e) + } + + details = if (subscriptionAddress == remoteAddressString) { + logInfo + } else { + "$logInfo $subscriptionAddress -> $remoteAddressString" + } + } + + return ClientHandshakeDriver(aeronDriver, pubSub!!, logInfo, details) + } + + @Throws(ClientTimedOutException::class) + private fun buildIPC( + shutdown: AtomicBoolean, + aeronDriver: AeronDriver, + handshakeTimeoutNs: Long, + sessionIdPub: Int, + streamIdPub: Int, + streamIdSub: Int, + reliable: Boolean, + tagName: String, + logInfo: String, + ): PubSub { + // Create a publication at the given address and port, using the given stream ID. + // Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs. + val publicationUri = uri(CommonContext.IPC_MEDIA, sessionIdPub, reliable) + + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be long running or re-entrant with the client. + + // For publications, if we add them "too quickly" (faster than the 'linger' timeout), Aeron will throw exceptions. + // ESPECIALLY if it is with the same streamID + // this check is in the "reconnect" logic + + // can throw an exception! We catch it in the calling class + val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, true) + + // can throw an exception! We catch it in the calling class + // we actually have to wait for it to connect before we continue + aeronDriver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause -> + ClientTimedOutException("$logInfo publication cannot connect with server in ${Sys.getTimePrettyFull(handshakeTimeoutNs)}", cause) + } + + // Create a subscription at the given address and port, using the given stream ID. + val subscriptionUri = uriHandshake(CommonContext.IPC_MEDIA, reliable) + val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, true) + + return PubSub( + pub = publication, + sub = subscription, + sessionIdPub = sessionIdPub, + sessionIdSub = 0, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + reliable = reliable, + remoteAddress = null, + remoteAddressString = EndPoint.IPC_NAME, + portPub = 0, + portSub = 0, + tagName = tagName + ) + } + + @Throws(ClientTimedOutException::class) + private fun buildUDP( + shutdown: AtomicBoolean, + aeronDriver: AeronDriver, + handshakeTimeoutNs: Long, + remoteAddress: InetAddress, + remoteAddressString: String, + portPub: Int, // this is the port1 value from the server + portSub: Int, + port2Server: Int, // this is the port2 value from the server + sessionIdPub: Int, + streamIdPub: Int, + reliable: Boolean, + streamIdSub: Int, + tagName: String, + logInfo: String, + ): PubSub { + @Suppress("NAME_SHADOWING") + var portSub = portSub + + // on close, the publication CAN linger (in case a client goes away, and then comes back) + // AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) + + val isRemoteIpv4 = remoteAddress is Inet4Address + + // Create a publication at the given address and port, using the given stream ID. + // ANY sessionID for the publication will work, because the SERVER doesn't have it defined + val publicationUri = uri(CommonContext.UDP_MEDIA, sessionIdPub, reliable) + .endpoint(isRemoteIpv4, remoteAddressString, portPub) + + + // For publications, if we add them "too quickly" (faster than the 'linger' timeout), Aeron will throw exceptions. + // ESPECIALLY if it is with the same streamID. This was noticed as a problem with IPC + + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be long running or re-entrant with the client. + + + // can throw an exception! We catch it in the calling class + val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, false) + + // can throw an exception! We catch it in the calling class + // we actually have to wait for it to connect before we continue + aeronDriver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause -> + streamIdAllocator.free(streamIdSub) // we don't continue, so close this as well + ClientTimedOutException("$logInfo publication cannot connect with server in ${Sys.getTimePrettyFull(handshakeTimeoutNs)}", cause) + } + + + // this will cause us to listen on the interface that connects with the remote address, instead of ALL interfaces. + val localAddressString = getLocalAddressString(publication, isRemoteIpv4) + + + // Create a subscription the given address and port, using the given stream ID. + var subscription: Subscription? = null + + if (portSub > -1) { + // this means we have EXPLICITLY defined a port, we must try to use it + + // A control endpoint for the subscriptions will cause a periodic service management "heartbeat" to be sent to the + // remote endpoint publication, which permits the remote publication to send us data, thereby getting us around NAT + val subscriptionUri = uriHandshake(CommonContext.UDP_MEDIA, reliable) + .endpoint(isRemoteIpv4, localAddressString, portSub) + .controlEndpoint(isRemoteIpv4, remoteAddressString, port2Server) + .controlMode(CommonContext.MDC_CONTROL_MODE_DYNAMIC) + + subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, false) + } else { + // randomly select what port should be used + var retryCount = 100 + val random = CryptoManagement.secureRandom + val isSameMachine = remoteAddress.isLoopbackAddress || remoteAddress == EndPoint.lanAddress + + portSub = random.nextInt(Short.MAX_VALUE-1025) + 1025 + while (subscription == null && retryCount-- > 0) { + // find a random port to bind to if we are loopback OR if we are the same IP address (not loopback, but to ourselves) + if (isSameMachine) { + // range from 1025-65534 + portSub = random.nextInt(Short.MAX_VALUE-1025) + 1025 + } + + try { + // A control endpoint for the subscriptions will cause a periodic service management "heartbeat" to be sent to the + // remote endpoint publication, which permits the remote publication to send us data, thereby getting us around NAT + val subscriptionUri = uriHandshake(CommonContext.UDP_MEDIA, reliable) + .endpoint(isRemoteIpv4, localAddressString, portSub) + .controlEndpoint(isRemoteIpv4, remoteAddressString, port2Server) + .controlMode(CommonContext.MDC_CONTROL_MODE_DYNAMIC) + + subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, false) + } catch (ignored: Exception) { + // whoops keep retrying!! + } + } + } + + if (subscription == null) { + val ex = ClientTimedOutException("Cannot create subscription port $logInfo. All attempted ports are invalid") + ex.cleanAllStackTrace() + throw ex + } + + return PubSub( + pub = publication, + sub = subscription, + sessionIdPub = sessionIdPub, + sessionIdSub = 0, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + reliable = reliable, + remoteAddress = remoteAddress, + remoteAddressString = remoteAddressString, + portPub = portPub, + portSub = portSub, + tagName = tagName + ) + } + } + + fun close(endpoint: EndPoint<*>) { + // only the subs are allocated on the client! +// sessionIdAllocator.free(pubSub.sessionIdPub) +// sessionIdAllocator.free(sessionIdSub) +// streamIdAllocator.free(streamIdPub) + streamIdAllocator.free(pubSub.streamIdSub) + + // on close, we want to make sure this file is DELETED! + + // we might not be able to close these connections. + try { + aeronDriver.close(pubSub.sub, logInfo) + } + catch (e: Exception) { + endpoint.listenerManager.notifyError(e) + } + try { + aeronDriver.close(pubSub.pub, logInfo) + } + catch (e: Exception) { + endpoint.listenerManager.notifyError(e) + } + } +} diff --git a/src/dorkbox/network/handshake/ConnectionCounts.kt b/src/dorkbox/network/handshake/ConnectionCounts.kt index b6ade567..1270c571 100644 --- a/src/dorkbox/network/handshake/ConnectionCounts.kt +++ b/src/dorkbox/network/handshake/ConnectionCounts.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.handshake import org.agrona.collections.Object2IntHashMap @@ -9,18 +25,22 @@ import java.net.InetAddress internal class ConnectionCounts { private val connectionsPerIpCounts = Object2IntHashMap(-1) + @Synchronized fun get(inetAddress: InetAddress): Int { return connectionsPerIpCounts.getOrPut(inetAddress) { 0 } } + @Synchronized fun increment(inetAddress: InetAddress, currentCount: Int) { connectionsPerIpCounts[inetAddress] = currentCount + 1 } + @Synchronized fun decrement(inetAddress: InetAddress, currentCount: Int) { connectionsPerIpCounts[inetAddress] = currentCount - 1 } + @Synchronized fun decrementSlow(inetAddress: InetAddress) { if (connectionsPerIpCounts.containsKey(inetAddress)) { val defaultVal = connectionsPerIpCounts.getValue(inetAddress) @@ -28,11 +48,13 @@ internal class ConnectionCounts { } } + @Synchronized fun isEmpty(): Boolean { return connectionsPerIpCounts.isEmpty() } + @Synchronized override fun toString(): String { - return connectionsPerIpCounts.entries.joinToString() + return connectionsPerIpCounts.entries.map { it.key }.joinToString() } } diff --git a/src/dorkbox/network/handshake/HandshakeMessage.kt b/src/dorkbox/network/handshake/HandshakeMessage.kt index 2a22fd72..925703ee 100644 --- a/src/dorkbox/network/handshake/HandshakeMessage.kt +++ b/src/dorkbox/network/handshake/HandshakeMessage.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,18 +32,16 @@ internal class HandshakeMessage private constructor() { // -1 means there is an error var state = INVALID + // used to name a connection (via the client) + var tag: String = "" + var errorMessage: String? = null - var subscriptionPort = 0 + var port = 0 var streamId = 0 var sessionId = 0 - // by default, this will be a reliable connection. When the client connects to the server, the client will specify if the new connection - // is a reliable/unreliable connection when setting up the MediaDriverConnection - val isReliable = true - - // the client sends its registration data to the server to make sure that the registered classes are the same between the client/server var registrationData: ByteArray? = null @@ -56,14 +54,15 @@ internal class HandshakeMessage private constructor() { const val DONE = 3 const val DONE_ACK = 4 - fun helloFromClient(connectKey: Long, publicKey: ByteArray, sessionId: Int, subscriptionPort: Int, streamId: Int): HandshakeMessage { + fun helloFromClient(connectKey: Long, publicKey: ByteArray, streamIdSub: Int, portSub: Int, tagName: String): HandshakeMessage { val hello = HandshakeMessage() hello.state = HELLO hello.connectKey = connectKey // this is 'bounced back' by the server, so the client knows if it's the correct connection message hello.publicKey = publicKey - hello.sessionId = sessionId - hello.subscriptionPort = subscriptionPort - hello.streamId = streamId + hello.sessionId = 0 // not used by the server, since it connects in a different way! + hello.streamId = streamIdSub + hello.port = portSub + hello.tag = tagName return hello } @@ -81,12 +80,12 @@ internal class HandshakeMessage private constructor() { return hello } - fun doneFromClient(connectKey: Long, subscriptionPort: Int, streamId: Int): HandshakeMessage { + fun doneFromClient(connectKey: Long, sessionIdSub: Int, streamIdSub: Int): HandshakeMessage { val hello = HandshakeMessage() hello.state = DONE hello.connectKey = connectKey // THIS MUST NEVER CHANGE! (the server/client expect this) - hello.subscriptionPort = subscriptionPort - hello.streamId = streamId + hello.sessionId = sessionIdSub + hello.streamId = streamIdSub return hello } @@ -134,6 +133,12 @@ internal class HandshakeMessage private constructor() { ", Error: $errorMessage" } - return "HandshakeMessage($stateStr$errorMsg)" + val connectInfo = if (connectKey != 0L) { + ", key=$connectKey" + } else { + "" + } + + return "HandshakeMessage($tag :: $stateStr$errorMsg sessionId=$sessionId, streamId=$streamId, port=$port${connectInfo})" } } diff --git a/src/dorkbox/network/handshake/Handshaker.kt b/src/dorkbox/network/handshake/Handshaker.kt new file mode 100644 index 00000000..6e3cea7c --- /dev/null +++ b/src/dorkbox/network/handshake/Handshaker.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.handshake + +import dorkbox.network.Configuration +import dorkbox.network.aeron.AeronDriver +import dorkbox.network.connection.Connection +import dorkbox.network.connection.ListenerManager +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace +import dorkbox.network.exceptions.ClientException +import dorkbox.network.exceptions.ServerException +import dorkbox.network.serialization.KryoReader +import dorkbox.network.serialization.KryoWriter +import dorkbox.network.serialization.Serialization +import io.aeron.Publication +import io.aeron.logbuffer.FrameDescriptor +import org.agrona.DirectBuffer +import org.agrona.concurrent.IdleStrategy +import org.slf4j.Logger + +internal class Handshaker( + private val logger: Logger, + config: Configuration, + serialization: Serialization, + private val listenerManager: ListenerManager, + val aeronDriver: AeronDriver, + val newException: (String, Throwable?) -> Throwable +) { + private val handshakeReadKryo: KryoReader + private val handshakeWriteKryo: KryoWriter + private val handshakeSendIdleStrategy: IdleStrategy + + init { + val maxMessageSize = FrameDescriptor.computeMaxMessageLength(config.publicationTermBufferLength) + + // All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems. + + handshakeReadKryo = KryoReader(maxMessageSize) + handshakeWriteKryo = KryoWriter(maxMessageSize) + + serialization.newHandshakeKryo(handshakeReadKryo) + serialization.newHandshakeKryo(handshakeWriteKryo) + + handshakeSendIdleStrategy = config.sendIdleStrategy + } + + /** + * NOTE: this **MUST** stay on the same co-routine that calls "send". This cannot be re-dispatched onto a different coroutine! + * CANNOT be called in action dispatch. ALWAYS ON SAME THREAD + * Server -> will be network polling thread + * Client -> will be thread that calls `connect()` + * + * @return true if the message was successfully sent by aeron + */ + @Suppress("DuplicatedCode") + internal fun writeMessage(publication: Publication, logInfo: String, message: HandshakeMessage): Boolean { + // The handshake sessionId IS NOT globally unique + if (logger.isTraceEnabled) { + logger.trace("[$logInfo] (${message.connectKey}) send HS: $message") + } + + try { + val buffer = handshakeWriteKryo.write(message) + + return aeronDriver.send(publication, buffer, logInfo, listenerManager, handshakeSendIdleStrategy) + } catch (e: Exception) { + // if the driver is closed due to a network disconnect or a remote-client termination, we also must close the connection. + if (aeronDriver.internal.mustRestartDriverOnError) { + // we had a HARD network crash/disconnect, we close the driver and then reconnect automatically + //NOTE: notifyDisconnect IS NOT CALLED! + } + else if (e is ClientException || e is ServerException) { + throw e + } + else { + val exception = newException("[$logInfo] Error serializing handshake message $message", e) + exception.cleanStackTrace(2) // 2 because we do not want to see the stack for the abstract `newException` + listenerManager.notifyError(exception) + throw exception + } + + return false + } finally { + handshakeSendIdleStrategy.reset() + } + } + + /** + * NOTE: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD + * + * THROWS EXCEPTION IF INVALID READS! + * + * @param buffer The buffer + * @param offset The offset from the start of the buffer + * @param length The number of bytes to extract + * + * @return the message + */ + internal fun readMessage(buffer: DirectBuffer, offset: Int, length: Int): Any? { + // NOTE: This ABSOLUTELY MUST be done on the same thread! This cannot be done on a new one, because the buffer could change! + return handshakeReadKryo.read(buffer, offset, length) + } +} diff --git a/src/dorkbox/network/handshake/PortAllocator.kt b/src/dorkbox/network/handshake/PortAllocator.kt deleted file mode 100644 index 6cd27d87..00000000 --- a/src/dorkbox/network/handshake/PortAllocator.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2020 dorkbox, llc - * - * 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 dorkbox.network.handshake - -import org.agrona.collections.IntArrayList - -/** - * An allocator for port numbers. - * - * The allocator accepts a base number `p` and a maximum count `n | n > 0`, and will allocate - * up to `n` numbers, in a random order, in the range `[p, p + n - 1`. - * - * @param basePort The base port - * @param numberOfPortsToAllocate The maximum number of ports that will be allocated - * - * @throws IllegalArgumentException If the port range is not valid - */ -class PortAllocator(basePort: Int, numberOfPortsToAllocate: Int) { - private val minPort: Int - private val maxPort: Int - - private val portShuffleReset: Int - private var portShuffleCount: Int - private val freePorts: IntArrayList - - init { - if (basePort !in 1..65535) { - throw IllegalArgumentException("Base port $basePort must be in the range [1, 65535]") - } - - minPort = basePort - maxPort = Math.max(basePort+1, basePort + (numberOfPortsToAllocate - 1)) - - if (maxPort !in (basePort + 1)..65535) { - throw IllegalArgumentException("Uppermost port $maxPort must be in the range [$basePort, 65535]") - } - - // every time we add 25% of ports back (via 'free'), reshuffle the ports - portShuffleReset = numberOfPortsToAllocate/4 - portShuffleCount = portShuffleReset - - freePorts = IntArrayList() - - for (port in basePort..maxPort) { - freePorts.addInt(port) - } - - freePorts.shuffle() - } - - /** - * Allocate `count` number of ports. - * - * @param count The number of ports that will be allocated - * - * @return An array of allocated ports - * - * @throws PortAllocationException If there are fewer than `count` ports available to allocate - */ - fun allocate(count: Int): IntArray { - if (freePorts.size < count) { - throw IllegalArgumentException("Too few ports available to allocate $count ports") - } - - // reshuffle the ports once we need to re-allocate a new port - if (portShuffleCount <= 0) { - portShuffleCount = portShuffleReset - freePorts.shuffle() - } - - val result = IntArray(count) - for (index in 0 until count) { - val lastValue = freePorts.size - 1 - val removed = freePorts.removeAt(lastValue) - result[index] = removed - } - - return result - } - - /** - * Frees the given ports. Has no effect if the given port is outside of the range considered by the allocator. - * - * @param ports The array of ports to free - */ - fun free(ports: IntArray) { - ports.forEach { - free(it) - } - } - - /** - * Free a given port. - *

- * Has no effect if the given port is outside of the range considered by the allocator. - * - * @param port The port - */ - fun free(port: Int) { - if (port in minPort..maxPort) { - // add at the end (so we don't have unnecessary array resizes) - freePorts.addInt(freePorts.size, port) - - portShuffleCount-- - } - } -} diff --git a/src/dorkbox/network/handshake/PubSub.kt b/src/dorkbox/network/handshake/PubSub.kt new file mode 100644 index 00000000..4f444f15 --- /dev/null +++ b/src/dorkbox/network/handshake/PubSub.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.handshake + +import dorkbox.network.connection.EndPoint +import io.aeron.Publication +import io.aeron.Subscription +import java.net.Inet4Address +import java.net.InetAddress + +data class PubSub( + val pub: Publication, + val sub: Subscription, + val sessionIdPub: Int, + val sessionIdSub: Int, + val streamIdPub: Int, + val streamIdSub: Int, + val reliable: Boolean, + val remoteAddress: InetAddress?, + val remoteAddressString: String, + val portPub: Int, + val portSub: Int, + val tagName: String // will either be "", or will be "[tag_name]" +) { + val isIpc get() = remoteAddress == null + + fun getLogInfo(extraDetails: Boolean): String { + return if (isIpc) { + val prefix = if (tagName.isNotEmpty()) { + EndPoint.IPC_NAME + " ($tagName)" + } else { + EndPoint.IPC_NAME + } + + if (extraDetails) { + "$prefix sessionID: p=${sessionIdPub} s=${sessionIdSub}, streamID: p=${streamIdPub} s=${streamIdSub}, reg: p=${pub.registrationId()} s=${sub.registrationId()}" + } else { + prefix + } + } else { + var prefix = if (remoteAddress is Inet4Address) { + "IPv4 $remoteAddressString" + } else { + "IPv6 $remoteAddressString" + } + + if (tagName.isNotEmpty()) { + prefix += " ($tagName)" + } + + if (extraDetails) { + "$prefix sessionID: p=${sessionIdPub} s=${sessionIdSub}, streamID: p=${streamIdPub} s=${streamIdSub}, port: p=${portPub} s=${portSub}, reg: p=${pub.registrationId()} s=${sub.registrationId()}" + } else { + prefix + } + } + } +} diff --git a/src/dorkbox/network/handshake/RandomId65kAllocator.kt b/src/dorkbox/network/handshake/RandomId65kAllocator.kt index c067a83c..a59a0c9c 100644 --- a/src/dorkbox/network/handshake/RandomId65kAllocator.kt +++ b/src/dorkbox/network/handshake/RandomId65kAllocator.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,12 @@ */ package dorkbox.network.handshake +import dorkbox.network.connection.CryptoManagement import dorkbox.network.exceptions.AllocationException import dorkbox.objectPool.ObjectPool import dorkbox.objectPool.Pool import kotlinx.atomicfu.atomic -import java.security.SecureRandom +import org.slf4j.LoggerFactory /** * An allocator for random IDs, the maximum number of IDs is an unsigned short (65535). @@ -32,28 +33,33 @@ import java.security.SecureRandom * @param min The minimum ID (inclusive) * @param max The maximum ID (exclusive) */ -class RandomId65kAllocator(private val min: Int = Integer.MIN_VALUE, max: Int = Integer.MAX_VALUE) { +class RandomId65kAllocator(private val min: Int, max: Int) { + constructor(size: Int): this(1, size + 1) + + companion object { + private val logger = LoggerFactory.getLogger("RandomId65k") + } + private val cache: Pool private val maxAssignments: Int private val assigned = atomic(0) - init { // IllegalArgumentException - require(max >= min) { - "Maximum value $max must be >= minimum value $min" - } + require(max >= min) { "Maximum value $max must be >= minimum value $min" } - maxAssignments = (max - min).coerceIn(1, Short.MAX_VALUE * 2) + val max65k = Short.MAX_VALUE * 2 + maxAssignments = (max - min).coerceIn(1, max65k) // create a shuffled list of ID's. This operation is ONLY performed ONE TIME per endpoint! + // Boxing the Ints here is OK, because they are boxed in the cache as well (so it doesn't matter). val ids = ArrayList(maxAssignments) - for (id in min..(min + maxAssignments - 1)) { + for (id in min until min + maxAssignments) { ids.add(id) } - ids.shuffle(SecureRandom()) + ids.shuffle(CryptoManagement.secureRandom) // populate the array of randomly assigned ID's. cache = ObjectPool.blocking(ids) @@ -71,8 +77,12 @@ class RandomId65kAllocator(private val min: Int = Integer.MIN_VALUE, max: Int = throw AllocationException("No IDs left to allocate") } - assigned.getAndIncrement() - return cache.take() + val count = assigned.incrementAndGet() + val id = cache.take() + if (logger.isTraceEnabled) { + logger.trace("Allocating $id (total $count)") + } + return id } /** @@ -83,13 +93,16 @@ class RandomId65kAllocator(private val min: Int = Integer.MIN_VALUE, max: Int = fun free(id: Int) { val assigned = assigned.decrementAndGet() if (assigned < 0) { - throw AllocationException("Unequal allocate/free method calls.") + throw AllocationException("Unequal allocate/free method calls attempting to free [$id] (too many 'free' calls).") + } + if (logger.isTraceEnabled) { + logger.trace("Freeing $id") } cache.put(id) } - fun isEmpty(): Boolean { - return assigned.value == 0 + fun counts(): Int { + return assigned.value } override fun toString(): String { diff --git a/src/dorkbox/network/handshake/ServerConnectionDriver.kt b/src/dorkbox/network/handshake/ServerConnectionDriver.kt new file mode 100644 index 00000000..aa7aab4f --- /dev/null +++ b/src/dorkbox/network/handshake/ServerConnectionDriver.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.handshake + +import dorkbox.network.aeron.AeronDriver +import dorkbox.network.aeron.AeronDriver.Companion.uri +import dorkbox.network.connection.EndPoint +import dorkbox.network.connection.IpInfo +import io.aeron.CommonContext +import java.net.Inet4Address +import java.net.InetAddress + +/** + * Set up the subscription + publication channels back to the client + * + * Note: this class is NOT closed the traditional way! It's pub/sub objects are used by the connection (which is where they are closed) + * + * This represents the connection PAIR between a server<->client + */ +internal class ServerConnectionDriver(val pubSub: PubSub) { + companion object { + fun build(isIpc: Boolean, + aeronDriver: AeronDriver, + sessionIdPub: Int, sessionIdSub: Int, + streamIdPub: Int, streamIdSub: Int, + + ipInfo: IpInfo, + remoteAddress: InetAddress?, + remoteAddressString: String, + portPubMdc: Int, portPub: Int, portSub: Int, + reliable: Boolean, + tagName: String, + logInfo: String): ServerConnectionDriver { + + val pubSub: PubSub + + if (isIpc) { + pubSub = buildIPC( + aeronDriver = aeronDriver, + sessionIdPub = sessionIdPub, + sessionIdSub = sessionIdSub, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + reliable = reliable, + tagName = tagName, + logInfo = logInfo + ) + } else { + pubSub = buildUdp( + aeronDriver = aeronDriver, + ipInfo = ipInfo, + sessionIdPub = sessionIdPub, + sessionIdSub = sessionIdSub, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + remoteAddress = remoteAddress!!, + remoteAddressString = remoteAddressString, + portPubMdc = portPubMdc, + portPub = portPub, + portSub = portSub, + reliable = reliable, + tagName = tagName, + logInfo = logInfo + ) + } + + return ServerConnectionDriver(pubSub) + } + + private fun buildIPC( + aeronDriver: AeronDriver, + sessionIdPub: Int, sessionIdSub: Int, + streamIdPub: Int, streamIdSub: Int, + reliable: Boolean, + tagName: String, + logInfo: String + ): PubSub { + // on close, the publication CAN linger (in case a client goes away, and then comes back) + // AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) + + // create a new publication for the connection (since the handshake ALWAYS closes the current publication) + val publicationUri = uri(CommonContext.IPC_MEDIA, sessionIdPub, reliable) + + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be long running or re-entrant with the client. + val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, true) + + // Create a subscription at the given address and port, using the given stream ID. + val subscriptionUri = uri(CommonContext.IPC_MEDIA, sessionIdSub, reliable) + val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, true) + + return PubSub( + pub = publication, + sub = subscription, + sessionIdPub = sessionIdPub, + sessionIdSub = sessionIdSub, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + reliable = reliable, + remoteAddress = null, + remoteAddressString = EndPoint.IPC_NAME, + portPub = 0, + portSub = 0, + tagName = tagName + ) + } + + private fun buildUdp( + aeronDriver: AeronDriver, + ipInfo: IpInfo, + sessionIdPub: Int, sessionIdSub: Int, + streamIdPub: Int, streamIdSub: Int, + remoteAddress: InetAddress, remoteAddressString: String, + portPubMdc: Int, // this is the MDC port - used to dynamically discover the portPub value (but we manually save this info) + portPub: Int, + portSub: Int, + reliable: Boolean, + tagName: String, + logInfo: String + ): PubSub { + // on close, the publication CAN linger (in case a client goes away, and then comes back) + // AERON_PUBLICATION_LINGER_TIMEOUT, 5s by default (this can also be set as a URI param) + + // connection timeout of 0 doesn't matter. it is not used by the server + // the client address WILL BE either IPv4 or IPv6 + val isRemoteIpv4 = remoteAddress is Inet4Address + + // create a new publication for the connection (since the handshake ALWAYS closes the current publication) + + // we explicitly have the publisher "connect to itself", because we are using MDC to work around NAT + + // A control endpoint for the subscriptions will cause a periodic service management "heartbeat" to be sent to the + // remote endpoint publication, which permits the remote publication to send us data, thereby getting us around NAT + val publicationUri = uri(CommonContext.UDP_MEDIA, sessionIdPub, reliable) + .controlEndpoint(ipInfo.getAeronPubAddress(isRemoteIpv4) + ":" + portPubMdc) // this is the control port! (listens to status messages and NAK from client) + + + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be long running or re-entrant with the client. + val publication = aeronDriver.addPublication(publicationUri, streamIdPub, logInfo, false) + + // if we are IPv6 WILDCARD -- then our subscription must ALSO be IPv6, even if our connection is via IPv4 + + // Create a subscription at the given address and port, using the given stream ID. + val subscriptionUri = uri(CommonContext.UDP_MEDIA, sessionIdSub, reliable) + .endpoint(ipInfo.formattedListenAddressString + ":" + portSub) + + + val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, false) + + return PubSub( + pub = publication, + sub = subscription, + sessionIdPub = sessionIdPub, + sessionIdSub = sessionIdSub, + streamIdPub = streamIdPub, + streamIdSub = streamIdSub, + reliable = reliable, + remoteAddress = remoteAddress, + remoteAddressString = remoteAddressString, + portPub = portPub, + portSub = portSub, + tagName = tagName + ) + } + } +} diff --git a/src/dorkbox/network/handshake/ServerHandshake.kt b/src/dorkbox/network/handshake/ServerHandshake.kt index 4e47978d..9ee5c9a4 100644 --- a/src/dorkbox/network/handshake/ServerHandshake.kt +++ b/src/dorkbox/network/handshake/ServerHandshake.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,24 +18,20 @@ package dorkbox.network.handshake import dorkbox.network.Server import dorkbox.network.ServerConfiguration import dorkbox.network.aeron.AeronDriver -import dorkbox.network.aeron.mediaDriver.MediaDriverConnectInfo -import dorkbox.network.aeron.mediaDriver.MediaDriverConnection -import dorkbox.network.aeron.mediaDriver.ServerIpcDriver -import dorkbox.network.aeron.mediaDriver.UdpMediaDriverPairedConnection -import dorkbox.network.connection.Connection -import dorkbox.network.connection.ConnectionParams -import dorkbox.network.connection.ListenerManager -import dorkbox.network.connection.PublicKeyValidationState +import dorkbox.network.aeron.AeronDriver.Companion.sessionIdAllocator +import dorkbox.network.aeron.AeronDriver.Companion.streamIdAllocator +import dorkbox.network.connection.* import dorkbox.network.exceptions.AllocationException +import dorkbox.network.exceptions.ServerHandshakeException +import dorkbox.network.exceptions.ServerTimedoutException +import dorkbox.network.exceptions.TransmitException import io.aeron.Publication -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import mu.KLogger import net.jodah.expiringmap.ExpirationPolicy import net.jodah.expiringmap.ExpiringMap +import org.slf4j.Logger import java.net.Inet4Address import java.net.InetAddress +import java.util.* import java.util.concurrent.* @@ -46,45 +42,62 @@ import java.util.concurrent.* */ @Suppress("DuplicatedCode", "JoinDeclarationAndAssignment") internal class ServerHandshake( - private val logger: KLogger, private val config: ServerConfiguration, private val listenerManager: ListenerManager, - aeronDriver: AeronDriver + private val aeronDriver: AeronDriver, + private val eventDispatch: EventDispatcher ) { // note: the expire time here is a LITTLE longer than the expire time in the client, this way we can adjust for network lag if it's close private val pendingConnections = ExpiringMap.builder() - // we MUST include the publication linger timeout, otherwise we might encounter problems that are NOT REALLY problems - .expiration(TimeUnit.SECONDS.toNanos(config.connectionCloseTimeoutInSeconds.toLong() * 2) + aeronDriver.getLingerNs(), TimeUnit.NANOSECONDS) + .apply { + // connections are extremely difficult to diagnose when the connection timeout is short + val timeUnit = if (EndPoint.DEBUG_CONNECTIONS) { TimeUnit.HOURS } else { TimeUnit.NANOSECONDS } + + // we MUST include the publication linger timeout, otherwise we might encounter problems that are NOT REALLY problems + this.expiration(TimeUnit.SECONDS.toNanos(config.connectionCloseTimeoutInSeconds.toLong() * 2) + aeronDriver.lingerNs(), timeUnit) + } .expirationPolicy(ExpirationPolicy.CREATED) .expirationListener { clientConnectKey, connection -> // this blocks until it fully runs (which is ok. this is fast) - logger.error { "[${clientConnectKey} Connection (${connection.id}) Timed out waiting for registration response from client" } + listenerManager.notifyError(ServerTimedoutException("[${clientConnectKey} Connection (${connection.id}) Timed out waiting for registration response from client")) connection.close() } .build() - private val connectionsPerIpCounts = ConnectionCounts() + internal val connectionsPerIpCounts = ConnectionCounts() + + /** + * how long does the initial handshake take to connect + */ + internal var handshakeTimeoutNs: Long + + init { + // we MUST include the publication linger timeout, otherwise we might encounter problems that are NOT REALLY problems + var handshakeTimeoutNs = TimeUnit.SECONDS.toNanos(config.connectionCloseTimeoutInSeconds.toLong()) + aeronDriver.publicationConnectionTimeoutNs() + aeronDriver.lingerNs() - // guarantee that session/stream ID's will ALWAYS be unique! (there can NEVER be a collision!) - private val sessionIdAllocator = RandomId65kAllocator(AeronDriver.RESERVED_SESSION_ID_LOW, AeronDriver.RESERVED_SESSION_ID_HIGH) - private val streamIdAllocator = RandomId65kAllocator(1, Integer.MAX_VALUE) + if (EndPoint.DEBUG_CONNECTIONS) { + // connections are extremely difficult to diagnose when the connection timeout is short + handshakeTimeoutNs = TimeUnit.HOURS.toNanos(1) + } + this.handshakeTimeoutNs = handshakeTimeoutNs + } /** * @return true if we should continue parsing the incoming message, false if we should abort (as we are DONE processing data) */ // note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD. ONLY RESPONSES ARE ON ACTION DISPATCH! - private fun validateMessageTypeAndDoPending( + fun validateMessageTypeAndDoPending( server: Server, - actionDispatch: CoroutineScope, + handshaker: Handshaker, handshakePublication: Publication, message: HandshakeMessage, - connectionString: String, - aeronLogInfo: String, - logger: KLogger + logInfo: String, + logger: Logger ): Boolean { + // check to see if this sessionId is ALREADY in use by another connection! // this can happen if there are multiple connections from the SAME ip address (ie: localhost) if (message.state == HandshakeMessage.HELLO) { @@ -92,15 +105,17 @@ internal class ServerHandshake( val existingConnection = pendingConnections[message.connectKey] if (existingConnection != null) { - val existingAeronLogInfo = "${existingConnection.id}/${existingConnection.streamId}" + // Server is the "source", client mirrors the server // WHOOPS! tell the client that it needs to retry, since a DIFFERENT client has a handshake in progress with the same sessionId - logger.error { "[$existingAeronLogInfo - ${message.connectKey}] Connection from $connectionString had an in-use session ID! Telling client to retry." } + listenerManager.notifyError(ServerHandshakeException("[$existingConnection] (${message.connectKey}) Connection had an in-use session ID! Telling client to retry.")) try { - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, HandshakeMessage.retry("Handshake already in progress for sessionID!")) + handshaker.writeMessage(handshakePublication, + logInfo, + HandshakeMessage.retry("Handshake already in progress for sessionID!")) } catch (e: Error) { - logger.error(e) { "[$aeronLogInfo - $existingAeronLogInfo] Handshake error!" } + listenerManager.notifyError(ServerHandshakeException("[$existingConnection] Handshake error", e)) } return false } @@ -108,48 +123,47 @@ internal class ServerHandshake( // check to see if this is a pending connection if (message.state == HandshakeMessage.DONE) { - val existingConnection = pendingConnections.remove(message.connectKey) - if (existingConnection == null) { - logger.error { "[$aeronLogInfo - ${message.connectKey}] Error! Pending connection from client $connectionString was null, and cannot complete handshake!" } + val newConnection = pendingConnections.remove(message.connectKey) + if (newConnection == null) { + listenerManager.notifyError(ServerHandshakeException("[?????] (${message.connectKey}) Error! Pending connection from client was null, and cannot complete handshake!")) return true - } else { - val existingAeronLogInfo = "${existingConnection.id}/${existingConnection.streamId}" + } - logger.debug { "[$aeronLogInfo - $existingAeronLogInfo - ${message.connectKey}] Connection from $connectionString done with handshake." } + val connectionType = if (newConnection.enableBufferedMessages) { + "Buffered connection" + } else { + "Connection" + } - // called on connection.close() - existingConnection.closeAction = { - // clean up the resources associated with this connection when it's closed - logger.debug { "[$existingAeronLogInfo] freeing resources" } - existingConnection.cleanup(connectionsPerIpCounts, sessionIdAllocator, streamIdAllocator) + // Server is the "source", client mirrors the server + if (logger.isTraceEnabled) { + logger.trace("[${newConnection}] (${message.connectKey}) $connectionType (${newConnection.id}) done with handshake.") + } else if (logger.isDebugEnabled) { + logger.debug("[${newConnection}] $connectionType (${newConnection.id}) done with handshake.") + } - // this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback - actionDispatch.launch { - existingConnection.doNotifyDisconnect() - } - } + newConnection.setImage() - // before we finish creating the connection, we initialize it (in case there needs to be logic that happens-before `onConnect` calls occur - listenerManager.notifyInit(existingConnection) + // before we finish creating the connection, we initialize it (in case there needs to be logic that happens-before `onConnect` calls + listenerManager.notifyInit(newConnection) - // this enables the connection to start polling for messages - server.addConnection(existingConnection) + // this enables the connection to start polling for messages + server.addConnection(newConnection) - // now tell the client we are done - try { - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, - HandshakeMessage.doneToClient(message.connectKey)) + // now tell the client we are done + try { + handshaker.writeMessage(handshakePublication, + logInfo, + HandshakeMessage.doneToClient(message.connectKey)) - // this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback - actionDispatch.launch { - listenerManager.notifyConnect(existingConnection) - } - } catch (e: Exception) { - logger.error(e) { "$aeronLogInfo - $existingAeronLogInfo - Handshake error!" } - } + listenerManager.notifyConnect(newConnection) - return false + newConnection.sendBufferedMessages() + } catch (e: Exception) { + listenerManager.notifyError(newConnection, TransmitException("[$newConnection] Handshake error", e)) } + + return false } return true @@ -161,24 +175,23 @@ internal class ServerHandshake( // note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD private fun validateUdpConnectionInfo( server: Server, + handshaker: Handshaker, handshakePublication: Publication, config: ServerConfiguration, - clientAddressString: String, clientAddress: InetAddress, - aeronLogInfo: String, - logger: KLogger + logInfo: String ): Boolean { try { // VALIDATE:: Check to see if there are already too many clients connected. - if (server.connections.connectionCount() >= config.maxClientCount) { - logger.error("[$aeronLogInfo] Connection from $clientAddressString not allowed! Server is full. Max allowed is ${config.maxClientCount}") + if (server.connections.size() >= config.maxClientCount) { + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Server is full. Max allowed is ${config.maxClientCount}")) try { - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, - HandshakeMessage.error("Server is full")) + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Server is full")) } catch (e: Exception) { - logger.error(e) { "[$aeronLogInfo] Handshake error!" } + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) } return false } @@ -186,29 +199,29 @@ internal class ServerHandshake( // VALIDATE:: we are now connected to the client and are going to create a new connection. val currentCountForIp = connectionsPerIpCounts.get(clientAddress) - if (currentCountForIp >= config.maxConnectionsPerIpAddress) { + if (config.maxConnectionsPerIpAddress in 1..currentCountForIp) { // decrement it now, since we aren't going to permit this connection (take the extra decrement hit on failure, instead of always) connectionsPerIpCounts.decrement(clientAddress, currentCountForIp) - logger.error { "[$aeronLogInfo] Too many connections for IP address $clientAddressString. Max allowed is ${config.maxConnectionsPerIpAddress}" } + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Too many connections for IP address. Max allowed is ${config.maxConnectionsPerIpAddress}")) try { - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, - HandshakeMessage.error("Too many connections for IP address")) + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Too many connections for IP address")) } catch (e: Exception) { - logger.error(e) { "[$aeronLogInfo] Handshake error!" } + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) } return false } connectionsPerIpCounts.increment(clientAddress, currentCountForIp) } catch (e: Exception) { - logger.error(e) { "[$aeronLogInfo] Could not validate client message" } + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Handshake error, Could not validate client message", e)) try { - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, - HandshakeMessage.error("Invalid connection")) + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Invalid connection")) } catch (e: Exception) { - logger.error(e) { "[$aeronLogInfo] Handshake error!" } + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) } } @@ -217,34 +230,28 @@ internal class ServerHandshake( /** - * @return true if the handshake poller is to close the publication, false will keep the publication (as we are DONE processing data) + * NOTE: This must not be called on the main thread because it is blocking! + * + * @return true if the connection was SUCCESS. False if the handshake poller should immediately close the publication */ fun processIpcHandshakeMessageServer( server: Server, + handshaker: Handshaker, + aeronDriver: AeronDriver, handshakePublication: Publication, + publicKey: ByteArray, message: HandshakeMessage, - aeronDriver: AeronDriver, - aeronLogInfo: String, - connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION, - logger: KLogger - ) { - - val connectionString = "IPC" - - if (!validateMessageTypeAndDoPending( - server, - server.actionDispatch, - handshakePublication, - message, - connectionString, - aeronLogInfo, - logger - )) { - return - } - + logInfo: String, + logger: Logger + ): Boolean { val serialization = config.serialization + val clientTagName = message.tag + if (clientTagName.length > 32) { + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Invalid tag name.")) + return false + } + ///// ///// ///// DONE WITH VALIDATION @@ -253,87 +260,134 @@ internal class ServerHandshake( // allocate session/stream id's - val connectionSessionId: Int + val connectionSessionIdPub: Int + try { + connectionSessionIdPub = sessionIdAllocator.allocate() + } catch (e: AllocationException) { + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a session pub ID for the client connection!", e)) + + try { + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Connection error!")) + } catch (e: Exception) { + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) + } + return false + } + + val connectionSessionIdSub: Int try { - connectionSessionId = sessionIdAllocator.allocate() + connectionSessionIdSub = sessionIdAllocator.allocate() } catch (e: AllocationException) { - logger.error { "[$aeronLogInfo] Connection from $connectionString not allowed! Unable to allocate a session ID for the client connection!" } + // have to unwind actions! + sessionIdAllocator.free(connectionSessionIdPub) + + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a session sub ID for the client connection!", e)) try { - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, - HandshakeMessage.error("Connection error!")) + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Connection error!")) } catch (e: Exception) { - logger.error(e) { "[$aeronLogInfo] Handshake error!" } + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) } - return + return false } - val connectionStreamPubId: Int + val connectionStreamIdPub: Int try { - connectionStreamPubId = streamIdAllocator.allocate() + connectionStreamIdPub = streamIdAllocator.allocate() } catch (e: AllocationException) { // have to unwind actions! - sessionIdAllocator.free(connectionSessionId) + sessionIdAllocator.free(connectionSessionIdPub) + sessionIdAllocator.free(connectionSessionIdSub) - logger.error { "[$aeronLogInfo] Connection from $connectionString not allowed! Unable to allocate a stream ID for the client connection!" } + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a stream publication ID for the client connection!", e)) try { - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, - HandshakeMessage.error("Connection error!")) + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Connection error!")) } catch (e: Exception) { - logger.error(e) { "[$aeronLogInfo] Handshake error!" } + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) } - return + return false } - val connectionStreamSubId: Int + val connectionStreamIdSub: Int try { - connectionStreamSubId = streamIdAllocator.allocate() + connectionStreamIdSub = streamIdAllocator.allocate() } catch (e: AllocationException) { // have to unwind actions! - sessionIdAllocator.free(connectionSessionId) - sessionIdAllocator.free(connectionStreamPubId) + sessionIdAllocator.free(connectionSessionIdPub) + sessionIdAllocator.free(connectionSessionIdSub) + streamIdAllocator.free(connectionStreamIdPub) - logger.error { "[$aeronLogInfo] Connection from $connectionString not allowed! Unable to allocate a stream ID for the client connection!" } + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a stream subscription ID for the client connection!", e)) try { - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, - HandshakeMessage.error("Connection error!")) + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Connection error!")) } catch (e: Exception) { - logger.error(e) { "[$aeronLogInfo] Handshake error!" } + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) } - return + return false } + // create a new connection. The session ID is encrypted. + var newConnection: CONNECTION? = null try { - // Create a subscription at the given address and port, using the given stream ID. - val driver = ServerIpcDriver(streamId = connectionStreamSubId, - sessionId = connectionSessionId) - driver.build(aeronDriver, logger) - - // create a new publication for the connection (since the handshake ALWAYS closes the current publication) - val publicationUri = MediaDriverConnection.uri("ipc", handshakePublication.sessionId()) - val clientPublication = aeronDriver.addPublication(publicationUri, message.subscriptionPort) - - val clientConnection = MediaDriverConnectInfo( - publication = clientPublication, - subscription = driver.subscription, - subscriptionPort = driver.streamId, - publicationPort = message.subscriptionPort, - streamId = 0, // this is because with IPC, we have stream sub/pub (which are replaced as port sub/pub) - sessionId = driver.sessionId, - isReliable = driver.isReliable, + // Create a pub/sub at the given address and port, using the given stream ID. + // NOTE: This must not be called on the main thread because it is blocking! + val newConnectionDriver = ServerConnectionDriver.build( + aeronDriver = aeronDriver, + ipInfo = server.ipInfo, + isIpc = true, + tagName = clientTagName, + logInfo = EndPoint.IPC_NAME, + remoteAddress = null, - remoteAddressString = "ipc" + remoteAddressString = "", + sessionIdPub = connectionSessionIdPub, + sessionIdSub = connectionSessionIdSub, + streamIdPub = connectionStreamIdPub, + streamIdSub = connectionStreamIdSub, + portPubMdc = 0, + portPub = 0, + portSub = 0, + reliable = true ) - logger.info { "[$aeronLogInfo] Creating new IPC connection from $driver" } + val enableBufferedMessagesForConnection = listenerManager.notifyEnableBufferedMessages(null, clientTagName) + val connectionType = if (enableBufferedMessagesForConnection) { + "buffered connection" + } else { + "connection" + } + + val connectionTypeCaps = connectionType.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + + + val logInfo = newConnectionDriver.pubSub.getLogInfo(logger.isDebugEnabled) + if (logger.isDebugEnabled) { + logger.debug("Creating new $connectionType to $logInfo") + } else { + logger.info("Creating new $connectionType to $logInfo") + } + + newConnection = server.newConnection(ConnectionParams( + publicKey = publicKey, + endPoint = server, + connectionInfo = newConnectionDriver.pubSub, + publicKeyValidation = PublicKeyValidationState.VALID, + enableBufferedMessages = enableBufferedMessagesForConnection, + cryptoKey = CryptoManagement.NOCRYPT // we don't use encryption for IPC connections + )) + + server.bufferedManager.onConnect(newConnection) - val connection = connectionFunc(ConnectionParams(server, clientConnection, PublicKeyValidationState.VALID)) // VALIDATE:: are we allowed to connect to this server (now that we have the initial server information) // NOTE: all IPC client connections are, by default, always allowed to connect, because they are running on the same machine @@ -344,82 +398,112 @@ internal class ServerHandshake( /////////////// - // The one-time pad is used to encrypt the session ID, so that ONLY the correct client knows what it is! val successMessage = HandshakeMessage.helloAckIpcToClient(message.connectKey) - // if necessary, we also send the kryo RMI id's that are registered as RMI on this endpoint, but maybe not on the other endpoint - - // now create the encrypted payload, using ECDH - val cryptOutput = server.crypto.cryptOutput - cryptOutput.reset() - cryptOutput.writeInt(connectionSessionId) - cryptOutput.writeInt(connectionStreamSubId) - - val regDetails = serialization.getKryoRegistrationDetails() - cryptOutput.writeInt(regDetails.size) - cryptOutput.writeBytes(regDetails) + // Also send the RMI registration data to the client (so the client doesn't register anything) - successMessage.registrationData = cryptOutput.toBytes() + // now create the encrypted payload, using no crypto + successMessage.registrationData = server.crypto.nocrypt( + sessionIdPub = connectionSessionIdPub, + sessionIdSub = connectionSessionIdSub, + streamIdPub = connectionStreamIdPub, + streamIdSub = connectionStreamIdSub, + sessionTimeout = config.bufferedConnectionTimeoutSeconds, + bufferedMessages = enableBufferedMessagesForConnection, + kryoRegDetails = serialization.getKryoRegistrationDetails() + ) successMessage.publicKey = server.crypto.publicKeyBytes // before we notify connect, we have to wait for the client to tell us that they can receive data - pendingConnections[message.connectKey] = connection + pendingConnections[message.connectKey] = newConnection + + if (logger.isTraceEnabled) { + logger.trace("[$logInfo] (${message.connectKey}) $connectionType (${newConnection.id}) responding to handshake hello.") + } else if (logger.isDebugEnabled) { + logger.debug("[$logInfo] $connectionTypeCaps (${newConnection.id}) responding to handshake hello.") + } // this tells the client all the info to connect. - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, successMessage) // exception is already caught! + handshaker.writeMessage(handshakePublication, logInfo, successMessage) // exception is already caught! } catch (e: Exception) { // have to unwind actions! - sessionIdAllocator.free(connectionSessionId) - streamIdAllocator.free(connectionStreamPubId) + sessionIdAllocator.free(connectionSessionIdPub) + sessionIdAllocator.free(connectionSessionIdSub) + streamIdAllocator.free(connectionStreamIdSub) + streamIdAllocator.free(connectionStreamIdPub) + + listenerManager.notifyError(ServerHandshakeException("[$logInfo] (${message.connectKey}) Connection (${newConnection?.id}) handshake crashed! Message $message", e)) - logger.error(e) { "[$aeronLogInfo] Connection handshake from $connectionString crashed! Message $message" } + return false } + + return true } /** - * note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD - * @return true if the handshake poller is to close the publication, false will keep the publication + * NOTE: This must not be called on the main thread because it is blocking! + * + * @return true if the connection was SUCCESS. False if the handshake poller should immediately close the publication */ fun processUdpHandshakeMessageServer( server: Server, + handshaker: Handshaker, handshakePublication: Publication, + publicKey: ByteArray, clientAddress: InetAddress, clientAddressString: String, + portSub: Int, + portPub: Int, + mdcPortPub: Int, isReliable: Boolean, message: HandshakeMessage, - aeronDriver: AeronDriver, - aeronLogInfo: String, - isIpv6Wildcard: Boolean, - connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION, - logger: KLogger - ) { - // Manage the Handshake state - if (!validateMessageTypeAndDoPending( - server, server.actionDispatch, handshakePublication, message, - clientAddressString, aeronLogInfo, logger)) - { - return - } + logInfo: String, + logger: Logger + ): Boolean { + val serialization = config.serialization + // UDP ONLY val clientPublicKeyBytes = message.publicKey val validateRemoteAddress: PublicKeyValidationState - val serialization = config.serialization // VALIDATE:: check to see if the remote connection's public key has changed! validateRemoteAddress = server.crypto.validateRemoteAddress(clientAddress, clientAddressString, clientPublicKeyBytes) if (validateRemoteAddress == PublicKeyValidationState.INVALID) { - logger.error { "[$aeronLogInfo] Connection from $clientAddressString not allowed! Public key mismatch." } - return + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Public key mismatch.")) + return false } + clientPublicKeyBytes!! + + val isSelfMachine = clientAddress.isLoopbackAddress || clientAddress == EndPoint.lanAddress - if (!clientAddress.isLoopbackAddress && - !validateUdpConnectionInfo(server, handshakePublication, config, clientAddressString, clientAddress, aeronLogInfo, logger)) { + if (!isSelfMachine && + !validateUdpConnectionInfo(server, handshaker, handshakePublication, config, clientAddress, logInfo)) { // we do not want to limit the loopback addresses! - return + return false + } + + val clientTagName = message.tag + if (clientTagName.length > 32) { + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Invalid tag name.")) + return false + } + + // VALIDATE:: are we allowed to connect to this server (now that we have the initial server information) + val permitConnection = listenerManager.notifyFilter(clientAddress, clientTagName) + if (!permitConnection) { + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection was not permitted!")) + + try { + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Connection was not permitted!")) + } catch (e: Exception) { + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) + } + return false } @@ -431,121 +515,152 @@ internal class ServerHandshake( // allocate session/stream id's - val connectionSessionId: Int + val connectionSessionIdPub: Int try { - connectionSessionId = sessionIdAllocator.allocate() + connectionSessionIdPub = sessionIdAllocator.allocate() } catch (e: AllocationException) { // have to unwind actions! connectionsPerIpCounts.decrementSlow(clientAddress) - logger.error { "[$aeronLogInfo] Connection from $clientAddressString not allowed! Unable to allocate a session ID for the client connection!" } + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a session ID for the client connection!")) try { - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, - HandshakeMessage.error("Connection error!")) + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Connection error!")) } catch (e: Exception) { - logger.error(e) { "[$aeronLogInfo] Handshake error!" } + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) } - return + return false } - val connectionStreamId: Int + val connectionSessionIdSub: Int try { - connectionStreamId = streamIdAllocator.allocate() + connectionSessionIdSub = sessionIdAllocator.allocate() } catch (e: AllocationException) { // have to unwind actions! connectionsPerIpCounts.decrementSlow(clientAddress) - sessionIdAllocator.free(connectionSessionId) + sessionIdAllocator.free(connectionSessionIdPub) - logger.error { "[$aeronLogInfo] Connection from $clientAddressString not allowed! Unable to allocate a stream ID for the client connection!" } + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a session ID for the client connection!")) try { - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, - HandshakeMessage.error("Connection error!")) + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Connection error!")) } catch (e: Exception) { - logger.error(e) { "[$aeronLogInfo] Handshake error!" } + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) } - return + return false } - // the pub/sub do not necessarily have to be the same. They can be ANY port - val publicationPort = message.subscriptionPort - val subscriptionPort = config.port + val connectionStreamIdPub: Int + try { + connectionStreamIdPub = streamIdAllocator.allocate() + } catch (e: AllocationException) { + // have to unwind actions! + connectionsPerIpCounts.decrementSlow(clientAddress) + sessionIdAllocator.free(connectionSessionIdPub) + sessionIdAllocator.free(connectionSessionIdSub) + + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a stream ID for the client connection!")) - // create a new connection. The session ID is encrypted. - var connection: CONNECTION? = null + try { + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Connection error!")) + } catch (e: Exception) { + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) + } + return false + } + + val connectionStreamIdSub: Int try { - // connection timeout of 0 doesn't matter. it is not used by the server - // the client address WILL BE either IPv4 or IPv6 - val listenAddress = if (clientAddress is Inet4Address && !isIpv6Wildcard) { - server.listenIPv4Address!! - } else { - // wildcard is SPECIAL, in that if we bind wildcard, it will ALSO bind to IPv4, so we can't bind both! - server.listenIPv6Address!! + connectionStreamIdSub = streamIdAllocator.allocate() + } catch (e: AllocationException) { + // have to unwind actions! + connectionsPerIpCounts.decrementSlow(clientAddress) + sessionIdAllocator.free(connectionSessionIdPub) + sessionIdAllocator.free(connectionSessionIdSub) + streamIdAllocator.free(connectionStreamIdPub) + + listenerManager.notifyError(ServerHandshakeException("[$logInfo] Connection not allowed! Unable to allocate a stream ID for the client connection!")) + + try { + handshaker.writeMessage(handshakePublication, logInfo, + HandshakeMessage.error("Connection error!")) + } catch (e: Exception) { + listenerManager.notifyError(TransmitException("[$logInfo] Handshake error", e)) } + return false + } - // create a new publication for the connection (since the handshake ALWAYS closes the current publication) - val publicationUri = MediaDriverConnection.uriEndpoint("udp", message.sessionId, isReliable, clientAddress, clientAddressString, message.subscriptionPort) - val clientPublication = aeronDriver.addPublication(publicationUri, message.streamId) - - val driver = UdpMediaDriverPairedConnection( - listenAddress, - clientAddress, - clientAddressString, - publicationPort, - subscriptionPort, - connectionStreamId, - connectionSessionId, - 0, - isReliable, - clientPublication - ) - driver.build(aeronDriver, logger) - logger.info { "[$aeronLogInfo] Creating new connection from $driver" } - - val clientConnection = MediaDriverConnectInfo( - publication = driver.publication, - subscription = driver.subscription, - subscriptionPort = driver.port, - publicationPort = publicationPort, - streamId = driver.streamId, - sessionId = driver.sessionId, - isReliable = driver.isReliable, + val logType = if (clientAddress is Inet4Address) { + "IPv4" + } else { + "IPv6" + } + + // create a new connection. The session ID is encrypted. + var newConnection: CONNECTION? = null + try { + // Create a pub/sub at the given address and port, using the given stream ID. + // NOTE: This must not be called on the main thread because it is blocking! + val newConnectionDriver = ServerConnectionDriver.build( + ipInfo = server.ipInfo, + aeronDriver = aeronDriver, + isIpc = false, + logInfo = logType, + remoteAddress = clientAddress, - remoteAddressString = clientAddressString + remoteAddressString = clientAddressString, + sessionIdPub = connectionSessionIdPub, + sessionIdSub = connectionSessionIdSub, + streamIdPub = connectionStreamIdPub, + streamIdSub = connectionStreamIdSub, + portPubMdc = mdcPortPub, + portPub = portPub, + portSub = portSub, + tagName = clientTagName, + reliable = isReliable ) - connection = connectionFunc(ConnectionParams(server, clientConnection, validateRemoteAddress)) + val cryptoSecretKey = server.crypto.generateAesKey(clientPublicKeyBytes, clientPublicKeyBytes, server.crypto.publicKeyBytes) - // VALIDATE:: are we allowed to connect to this server (now that we have the initial server information) - val permitConnection = listenerManager.notifyFilter(connection) - if (!permitConnection) { - // have to unwind actions! - connectionsPerIpCounts.decrementSlow(clientAddress) - sessionIdAllocator.free(connectionSessionId) - streamIdAllocator.free(connectionStreamId) - connection.close() - logger.error { "[$aeronLogInfo] Connection $clientAddressString was not permitted!" } + val enableBufferedMessagesForConnection = listenerManager.notifyEnableBufferedMessages(clientAddress, clientTagName) + val connectionType = if (enableBufferedMessagesForConnection) { + "buffered connection" + } else { + "connection" + } - try { - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, - HandshakeMessage.error("Connection was not permitted!")) - } catch (e: Exception) { - logger.error(e) { "[$aeronLogInfo] Handshake error!" } - } - return + val connectionTypeCaps = connectionType.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + + val logInfo = newConnectionDriver.pubSub.getLogInfo(logger.isDebugEnabled) + if (logger.isDebugEnabled) { + logger.debug("Creating new $connectionType to $logInfo") + } else { + logger.info("Creating new $connectionType to $logInfo") } + newConnection = server.newConnection(ConnectionParams( + publicKey = publicKey, + endPoint = server, + connectionInfo = newConnectionDriver.pubSub, + publicKeyValidation = validateRemoteAddress, + enableBufferedMessages = enableBufferedMessagesForConnection, + cryptoKey = cryptoSecretKey + )) + + server.bufferedManager.onConnect(newConnection) + /////////////// /// HANDSHAKE /////////////// - // The one-time pad is used to encrypt the session ID, so that ONLY the correct client knows what it is! val successMessage = HandshakeMessage.helloAckToClient(message.connectKey) @@ -553,29 +668,43 @@ internal class ServerHandshake( // Also send the RMI registration data to the client (so the client doesn't register anything) // now create the encrypted payload, using ECDH - successMessage.registrationData = server.crypto.encrypt(clientPublicKeyBytes!!, - subscriptionPort, - connectionSessionId, - connectionStreamId, - serialization.getKryoRegistrationDetails()) + successMessage.registrationData = server.crypto.encrypt( + cryptoSecretKey = cryptoSecretKey, + sessionIdPub = connectionSessionIdPub, + sessionIdSub = connectionSessionIdSub, + streamIdPub = connectionStreamIdPub, + streamIdSub = connectionStreamIdSub, + sessionTimeout = config.bufferedConnectionTimeoutSeconds, + bufferedMessages = enableBufferedMessagesForConnection, + kryoRegDetails = serialization.getKryoRegistrationDetails() + ) successMessage.publicKey = server.crypto.publicKeyBytes // before we notify connect, we have to wait for the client to tell us that they can receive data - pendingConnections[message.connectKey] = connection + pendingConnections[message.connectKey] = newConnection - logger.debug { "[$aeronLogInfo - ${message.connectKey}] Connection (${connection.streamId}/${connection.id}) responding to handshake hello." } + if (logger.isTraceEnabled) { + logger.trace("[$logInfo] $connectionTypeCaps (${newConnection.id}) responding to handshake hello.") + } else if (logger.isDebugEnabled) { + logger.debug("[$logInfo] $connectionTypeCaps (${newConnection.id}) responding to handshake hello.") + } // this tells the client all the info to connect. - server.writeHandshakeMessage(handshakePublication, aeronLogInfo, successMessage) // exception is already caught + handshaker.writeMessage(handshakePublication, logInfo, successMessage) // exception is already caught } catch (e: Exception) { // have to unwind actions! connectionsPerIpCounts.decrementSlow(clientAddress) - sessionIdAllocator.free(connectionSessionId) - streamIdAllocator.free(connectionStreamId) + sessionIdAllocator.free(connectionSessionIdPub) + sessionIdAllocator.free(connectionSessionIdSub) + streamIdAllocator.free(connectionStreamIdPub) + streamIdAllocator.free(connectionStreamIdSub) - logger.error(e) { "[$aeronLogInfo - ${message.connectKey}] Connection (${connection?.streamId}/${connection?.id}) handshake from $clientAddressString crashed! Message $message" } + listenerManager.notifyError(ServerHandshakeException("[$logInfo] (${message.connectKey}) Connection (${newConnection?.id}) handshake crashed! Message $message", e)) + return false } + + return true } /** @@ -584,14 +713,11 @@ internal class ServerHandshake( * note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD */ fun checkForMemoryLeaks() { - val noAllocations = connectionsPerIpCounts.isEmpty() && sessionIdAllocator.isEmpty() && streamIdAllocator.isEmpty() + val noAllocations = connectionsPerIpCounts.isEmpty() if (!noAllocations) { - throw AllocationException("Unequal allocate/free method calls for validation. \n" + - "connectionsPerIpCounts: '$connectionsPerIpCounts' \n" + - "sessionIdAllocator: $sessionIdAllocator \n" + - "streamIdAllocator: $streamIdAllocator") - + throw AllocationException("Unequal allocate/free method calls for IP validation. \n" + + "connectionsPerIpCounts: '$connectionsPerIpCounts'") } } @@ -601,12 +727,17 @@ internal class ServerHandshake( * note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD */ fun clear() { - runBlocking { - pendingConnections.forEach { (_, v) -> + val connections = pendingConnections + val latch = CountDownLatch(connections.size) + + eventDispatch.CLOSE.launch { + connections.forEach { (_, v) -> v.close() + latch.countDown() } - - pendingConnections.clear() } + + latch.await(config.connectionCloseTimeoutInSeconds.toLong() * connections.size, TimeUnit.MILLISECONDS) + connections.clear() } } diff --git a/src/dorkbox/network/handshake/ServerHandshakeDriver.kt b/src/dorkbox/network/handshake/ServerHandshakeDriver.kt new file mode 100644 index 00000000..e9561db3 --- /dev/null +++ b/src/dorkbox/network/handshake/ServerHandshakeDriver.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.handshake + +import dorkbox.network.aeron.AeronDriver +import dorkbox.network.aeron.AeronDriver.Companion.uriHandshake +import dorkbox.network.connection.EndPoint +import dorkbox.network.connection.IpInfo +import io.aeron.ChannelUriStringBuilder +import io.aeron.CommonContext +import io.aeron.Subscription + +/** + * For a client, the ports specified here MUST be manually flipped because they are in the perspective of the SERVER. + * A connection timeout of 0, means to wait forever + */ +internal class ServerHandshakeDriver( + private val aeronDriver: AeronDriver, + val subscription: Subscription, + val info: String, + private val logInfo: String) +{ + companion object { + fun build( + aeronDriver: AeronDriver, + isIpc: Boolean, + ipInfo: IpInfo, + port: Int, + streamIdSub: Int, sessionIdSub: Int, + logInfo: String + ): ServerHandshakeDriver { + + val info: String + val subscriptionUri: ChannelUriStringBuilder + + if (isIpc) { + subscriptionUri = uriHandshake(CommonContext.IPC_MEDIA, true) + info = "$logInfo [$sessionIdSub|$streamIdSub]" + } else { + // are we ipv4 or ipv6 or ipv6wildcard? + subscriptionUri = uriHandshake(CommonContext.UDP_MEDIA, ipInfo.isReliable) + .endpoint(ipInfo.getAeronPubAddress(ipInfo.isIpv4) + ":" + port) + + info = "$logInfo ${ipInfo.listenAddressStringPretty}:$port [$sessionIdSub|$streamIdSub] (reliable:${ipInfo.isReliable})" + } + + val subscription = aeronDriver.addSubscription(subscriptionUri, streamIdSub, logInfo, isIpc) + return ServerHandshakeDriver(aeronDriver, subscription, info, logInfo) + } + } + + fun close(endPoint: EndPoint<*>) { + try { + // we might not be able to close this connection. + aeronDriver.close(subscription, logInfo) + } + catch (e: Exception) { + endPoint.listenerManager.notifyError(e) + } + } + + fun unsafeClose() { + // we might not be able to close this connection. + aeronDriver.close(subscription, logInfo) + } + + override fun toString(): String { + return info + } +} diff --git a/src/dorkbox/network/handshake/ServerHandshakePollers.kt b/src/dorkbox/network/handshake/ServerHandshakePollers.kt index b7d05de5..d0cf59a1 100644 --- a/src/dorkbox/network/handshake/ServerHandshakePollers.kt +++ b/src/dorkbox/network/handshake/ServerHandshakePollers.kt @@ -1,22 +1,49 @@ +/* + * Copyright 2024 dorkbox, llc + * + * 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. + */ + @file:Suppress("MemberVisibilityCanBePrivate", "DuplicatedCode") package dorkbox.network.handshake import dorkbox.netUtil.IP +import dorkbox.network.Configuration import dorkbox.network.Server import dorkbox.network.ServerConfiguration import dorkbox.network.aeron.AeronDriver +import dorkbox.network.aeron.AeronDriver.Companion.uriHandshake import dorkbox.network.aeron.AeronPoller -import dorkbox.network.aeron.mediaDriver.MediaDriverConnection -import dorkbox.network.aeron.mediaDriver.ServerIpcDriver -import dorkbox.network.aeron.mediaDriver.ServerUdpDriver import dorkbox.network.connection.Connection -import dorkbox.network.connection.ConnectionParams +import dorkbox.network.connection.IpInfo +import dorkbox.network.exceptions.ServerException +import dorkbox.network.exceptions.ServerHandshakeException +import dorkbox.network.exceptions.ServerTimedoutException +import dorkbox.util.NamedThreadFactory +import dorkbox.util.Sys +import io.aeron.CommonContext import io.aeron.FragmentAssembler import io.aeron.Image +import io.aeron.Publication +import io.aeron.logbuffer.FragmentHandler import io.aeron.logbuffer.Header -import mu.KLogger +import net.jodah.expiringmap.ExpirationPolicy +import net.jodah.expiringmap.ExpiringMap import org.agrona.DirectBuffer +import org.slf4j.Logger +import java.net.Inet4Address +import java.util.concurrent.* internal object ServerHandshakePollers { fun disabled(serverInfo: String): AeronPoller { @@ -27,286 +54,699 @@ internal object ServerHandshakePollers { } } - private fun ipcProcessing( - logger: KLogger, - server: Server, aeronDriver: AeronDriver, - header: Header, buffer: DirectBuffer, offset: Int, length: Int, - handshake: ServerHandshake, connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION - ) { - // this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe! - - // The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity. - // for the handshake, the sessionId IS NOT GLOBALLY UNIQUE - val sessionId = header.sessionId() - val streamId = header.streamId() - val aeronLogInfo = "$sessionId/$streamId" - - val message = server.readHandshakeMessage(buffer, offset, length, header, aeronLogInfo) - - // VALIDATE:: a Registration object is the only acceptable message during the connection phase - if (message !is HandshakeMessage) { - logger.error { "[$aeronLogInfo] Connection from IPC not allowed! Invalid connection request" } - } else { - // we create a NEW publication for the handshake, which connects directly to the client handshake subscription - val publicationUri = MediaDriverConnection.uri("ipc", message.sessionId) - val publication = aeronDriver.addPublication(publicationUri, message.subscriptionPort) - - handshake.processIpcHandshakeMessageServer( - server, publication, message, - aeronDriver, aeronLogInfo, - connectionFunc, logger - ) + class IpcProc( + val logger: Logger, + val server: Server, + val driver: AeronDriver, + val handshake: ServerHandshake + ): FragmentHandler { + + private val isReliable = server.config.isReliable + private val handshaker = server.handshaker + private val handshakeTimeoutNs = handshake.handshakeTimeoutNs + private val shutdownInProgress = server.shutdownInProgress + private val shutdown = server.shutdown + + // note: the expire time here is a LITTLE longer than the expire time in the client, this way we can adjust for network lag if it's close + private val publications = ExpiringMap.builder() + .apply { + this.expiration(handshakeTimeoutNs, TimeUnit.NANOSECONDS) + } + .expirationPolicy(ExpirationPolicy.CREATED) + .expirationListener { connectKey, publication -> + try { + // we might not be able to close this connection. + driver.close(publication, "Server IPC Handshake ($connectKey)") + } + catch (e: Exception) { + server.listenerManager.notifyError(e) + } + + } + .build() + + override fun onFragment(buffer: DirectBuffer, offset: Int, length: Int, header: Header) { + // this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe! + + // The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity. + // for the handshake, the sessionId IS NOT GLOBALLY UNIQUE + val sessionId = header.sessionId() + val streamId = header.streamId() + val image = header.context() as Image + + val logInfo = "$sessionId/$streamId : IPC" // Server is the "source", client mirrors the server + + if (shutdownInProgress.value) { + driver.deleteLogFile(image) + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] server is shutting down. Aborting new connection attempts.")) + return + } + + // ugh, this is verbose -- but necessary + val message = try { + val msg = handshaker.readMessage(buffer, offset, length) + + // VALIDATE:: a Registration object is the only acceptable message during the connection phase + if (msg !is HandshakeMessage) { + throw ServerHandshakeException("[$logInfo] Connection not allowed! unrecognized message: $msg") + } else if (logger.isTraceEnabled) { + logger.trace("[$logInfo] (${msg.connectKey}) received HS: $msg") + } + msg + } catch (e: Exception) { + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error de-serializing handshake message!!", e)) + null + } + + + if (message == null) { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + return + } + + + // NOTE: This MUST to happen in separates thread so that we can take as long as we need when creating publications and handshaking, + // because under load -- this will REGULARLY timeout! Under no circumstance can this happen in the main processing thread!! + server.eventDispatch.HANDSHAKE.launch { + // we have read all the data, now dispatch it. + // HandshakeMessage.HELLO + // HandshakeMessage.DONE + val messageState = message.state + val connectKey = message.connectKey + + + if (messageState == HandshakeMessage.HELLO) { + // we create a NEW publication for the handshake, which connects directly to the client handshake subscription + + val publicationUri = uriHandshake(CommonContext.IPC_MEDIA, isReliable) + + // this will always connect to the CLIENT handshake subscription! + val publication = try { + driver.addPublication(publicationUri, message.streamId, logInfo, true) + } + catch (e: Exception) { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Cannot create IPC publication back to client remote process", e)) + return@launch + } + + try { + // we actually have to wait for it to connect before we continue + driver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause -> + ServerTimedoutException("$logInfo publication cannot connect with client in ${Sys.getTimePrettyFull(handshakeTimeoutNs)}", cause) + } + } + catch (e: Exception) { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Cannot create IPC publication back to client remote process", e)) + return@launch + } + + + try { + val success = handshake.processIpcHandshakeMessageServer( + server = server, + handshaker = handshaker, + aeronDriver = driver, + handshakePublication = publication, + publicKey = message.publicKey!!, + message = message, + logInfo = logInfo, + logger = logger + ) + + if (success) { + publications[connectKey] = publication + } + else { + try { + // we might not be able to close this connection. + driver.close(publication, logInfo) + } + catch (e: Exception) { + server.listenerManager.notifyError(e) + } + } + } + catch (e: Exception) { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + + try { + // we might not be able to close this connection. + driver.close(publication, logInfo) + } + catch (e: Exception) { + server.listenerManager.notifyError(e) + } + + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error processing IPC handshake", e)) + } + } else { + // HandshakeMessage.DONE + + val publication = publications.remove(connectKey) + if (publication == null) { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] No publication back to IPC")) + return@launch + } + + try { + handshake.validateMessageTypeAndDoPending( + server = server, + handshaker = handshaker, + handshakePublication = publication, + message = message, + logInfo = logInfo, + logger = logger + ) + } catch (e: Exception) { + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error processing IPC handshake", e)) + } + + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + + try { + // we might not be able to close this connection. + driver.close(publication, logInfo) + } + catch (e: Exception) { + server.listenerManager.notifyError(e) + } + } + } + } - publication.close() + fun close() { + publications.forEach { (connectKey, publication) -> + AeronDriver.sessionIdAllocator.free(publication.sessionId()) + try { + // we might not be able to close this connection. + driver.close(publication, "Server Handshake ($connectKey)") + } + catch (e: Exception) { + server.listenerManager.notifyError(e) + } + } + publications.clear() } } - private fun ipProcessing( - logger: KLogger, - server: Server, isReliable: Boolean, aeronDriver: AeronDriver, isIpv6Wildcard: Boolean, - header: Header, buffer: DirectBuffer, offset: Int, length: Int, - handshake: ServerHandshake, connectionFunc: (connectionParameters: ConnectionParams) -> CONNECTION - ) { - // this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe! - - // The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity. - // for the handshake, the sessionId IS NOT GLOBALLY UNIQUE - val sessionId = header.sessionId() - val streamId = header.streamId() - val aeronLogInfo = "$sessionId/$streamId" - - // note: this address will ALWAYS be an IP:PORT combo OR it will be aeron:ipc (if IPC, it will be a different handler!) - val remoteIpAndPort = (header.context() as Image).sourceIdentity() - - // split - val splitPoint = remoteIpAndPort.lastIndexOf(':') - val clientAddressString = remoteIpAndPort.substring(0, splitPoint) - - val message = server.readHandshakeMessage(buffer, offset, length, header, aeronLogInfo) - - // VALIDATE:: a Registration object is the only acceptable message during the connection phase - if (message !is HandshakeMessage) { - logger.error { "[$aeronLogInfo] Connection from $clientAddressString not allowed! Invalid connection request" } - } else { + class UdpProc( + val logger: Logger, + val server: Server, + val driver: AeronDriver, + val handshake: ServerHandshake, + val isReliable: Boolean + ): FragmentHandler { + companion object { + init { + ExpiringMap.setThreadFactory(NamedThreadFactory("ExpiringMap-Eviction", Configuration.networkThreadGroup, true)) + } + } + + private val ipInfo = server.ipInfo + private val handshaker = server.handshaker + private val handshakeTimeoutNs = handshake.handshakeTimeoutNs + private val shutdownInProgress = server.shutdownInProgress + private val shutdown = server.shutdown + + private val serverPortSub = server.port1 + // MDC 'dynamic control mode' means that the server will to listen for status messages and NAK (from the client) on a port. + private val mdcPortPub = server.port2 + + // note: the expire time here is a LITTLE longer than the expire time in the client, this way we can adjust for network lag if it's close + private val publications = ExpiringMap.builder() + .apply { + + this.expiration(handshakeTimeoutNs, TimeUnit.NANOSECONDS) + } + .expirationPolicy(ExpirationPolicy.CREATED) + .expirationListener { connectKey, publication -> + try { + // we might not be able to close this connection. + driver.close(publication, "Server UDP Handshake ($connectKey)") + } + catch (e: Exception) { + server.listenerManager.notifyError(e) + } + } + .build() + + + override fun onFragment(buffer: DirectBuffer, offset: Int, length: Int, header: Header) { + // The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity. + // for the handshake, the sessionId IS NOT GLOBALLY UNIQUE + + + // this is processed on the thread that calls "poll". Subscriptions are NOT multi-thread safe! + + // The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity. + // for the handshake, the sessionId IS NOT GLOBALLY UNIQUE + val sessionId = header.sessionId() + val streamId = header.streamId() + val image = header.context() as Image + + // note: this address will ALWAYS be an IP:PORT combo OR it will be aeron:ipc (if IPC, it will be a different handler!) + val remoteIpAndPort = image.sourceIdentity() + + // split + val splitPoint = remoteIpAndPort.lastIndexOf(':') + var clientAddressString = remoteIpAndPort.substring(0, splitPoint) + // this should never be null, because we are feeding it a valid IP address from aeron val clientAddress = IP.toAddress(clientAddressString) if (clientAddress == null) { - logger.error { "[$aeronLogInfo] Connection from $clientAddressString not allowed! Invalid IP address!" } + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + + + // Server is the "source", client mirrors the server + server.listenerManager.notifyError(ServerHandshakeException("[$sessionId/$streamId] Connection from $clientAddressString not allowed! Invalid IP address!")) return } - // we create a NEW publication for the handshake, which connects directly to the client handshake subscription - val publicationUri = MediaDriverConnection.uriEndpoint("udp", message.sessionId, isReliable, clientAddress, clientAddressString, message.subscriptionPort) - val publication = aeronDriver.addPublication(publicationUri, message.streamId) + val isRemoteIpv4 = clientAddress is Inet4Address + if (!isRemoteIpv4) { + // this is necessary to clean up the address when adding it to aeron, since different formats mess it up + clientAddressString = IP.toString(clientAddress) - handshake.processUdpHandshakeMessageServer( - server, publication, clientAddress, clientAddressString, isReliable, message, - aeronDriver, aeronLogInfo, isIpv6Wildcard, - connectionFunc, logger - ) + if (ipInfo.ipType == IpInfo.Companion.IpListenType.IPv4Wildcard) { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) - publication.close() - } - } + // we DO NOT want to listen to IPv4 traffic, but we received IPv4 traffic! + server.listenerManager.notifyError(ServerHandshakeException("[$sessionId/$streamId] Connection from $clientAddressString not allowed! IPv4 connections not permitted!")) + return + } + } + val logInfo = "$sessionId/$streamId:$clientAddressString" + if (shutdownInProgress.value) { + driver.deleteLogFile(image) + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] server is shutting down. Aborting new connection attempts.")) + return + } - fun ipc( - aeronDriver: AeronDriver, config: ServerConfiguration, server: Server, handshake: ServerHandshake - ): AeronPoller - { - val logger = server.logger - val connectionFunc = server.connectionFunc + // ugh, this is verbose -- but necessary + val message = try { + val msg = handshaker.readMessage(buffer, offset, length) - val poller = if (config.enableIpc) { - val driver = ServerIpcDriver( - streamId = config.ipcId, - sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID - ) - driver.build(aeronDriver, logger) + // VALIDATE:: a Registration object is the only acceptable message during the connection phase + if (msg !is HandshakeMessage) { + throw ServerHandshakeException("[$logInfo] Connection not allowed! unrecognized message: $msg") + } else if (logger.isTraceEnabled) { + logger.trace("[$logInfo] (${msg.connectKey}) received HS: $msg") + } + msg + } catch (e: Exception) { + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error de-serializing handshake message!!", e)) + null + } - val subscription = driver.subscription + if (message == null) { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + return + } - object : AeronPoller { - val handler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header -> - ipcProcessing(logger, server, aeronDriver, header, buffer, offset, length, handshake, connectionFunc) + // NOTE: This MUST to happen in separates thread so that we can take as long as we need when creating publications and handshaking, + // because under load -- this will REGULARLY timeout! Under no circumstance can this happen in the main processing thread!! + server.eventDispatch.HANDSHAKE.launch { + // HandshakeMessage.HELLO + // HandshakeMessage.DONE + val messageState = message.state + val connectKey = message.connectKey + + if (messageState == HandshakeMessage.HELLO) { + // we create a NEW publication for the handshake, which connects directly to the client handshake subscription + + // we explicitly have the publisher "connect to itself", because we are using MDC to work around NAT. + // It will "auto-connect" to the correct client port (negotiated by the MDC client subscription negotiating on the + // control port of the server) + val publicationUri = uriHandshake(CommonContext.UDP_MEDIA, isReliable) + .controlEndpoint(ipInfo.getAeronPubAddress(isRemoteIpv4) + ":" + mdcPortPub) + + + // this will always connect to the CLIENT handshake subscription! + val publication = try { + driver.addPublication(publicationUri, message.streamId, logInfo, false) + } catch (e: Exception) { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Cannot create publication back to $clientAddressString", e)) + return@launch + } + + try { + // we actually have to wait for it to connect before we continue. + // + driver.waitForConnection(shutdown, publication, handshakeTimeoutNs, logInfo) { cause -> + ServerTimedoutException("$logInfo publication cannot connect with client in ${Sys.getTimePrettyFull(handshakeTimeoutNs)}", cause) + } + } catch (e: Exception) { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Cannot create publication back to $clientAddressString", e)) + return@launch + } + + try { + val success = handshake.processUdpHandshakeMessageServer( + server = server, + handshaker = handshaker, + handshakePublication = publication, + publicKey = message.publicKey!!, + clientAddress = clientAddress, + clientAddressString = clientAddressString, + portPub = message.port, + portSub = serverPortSub, + mdcPortPub = mdcPortPub, + isReliable = isReliable, + message = message, + logInfo = logInfo, + logger = logger + ) + + if (success) { + publications[connectKey] = publication + } else { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + + try { + // we might not be able to close this connection. + driver.close(publication, logInfo) + } + catch (e: Exception) { + server.listenerManager.notifyError(e) + } + } + } catch (e: Exception) { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + + try { + // we might not be able to close this connection. + driver.close(publication, logInfo) + } + catch (e: Exception) { + driver.close(publication, logInfo) + } + + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error processing IPC handshake", e)) + } + } else { + // HandshakeMessage.DONE + + val publication = publications.remove(connectKey) + + if (publication == null) { + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) + + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] No publication back to $clientAddressString")) + return@launch + } + + try { + handshake.validateMessageTypeAndDoPending( + server = server, + handshaker = handshaker, + handshakePublication = publication, + message = message, + logInfo = logInfo, + logger = logger + ) + } catch (e: Exception) { + server.listenerManager.notifyError(ServerHandshakeException("[$logInfo] Error processing IPC handshake", e)) + } + + try { + // we might not be able to close this connection. + driver.close(publication, logInfo) + } + catch (e: Exception) { + server.listenerManager.notifyError(e) + } + + // we should immediately remove the logbuffer for this! Aeron will **EVENTUALLY** remove the logbuffer, but if errors + // and connections occur too quickly (within the cleanup/linger period), we can run out of memory! + driver.deleteLogFile(image) } + } + } + + fun close() { + publications.forEach { (connectKey, publication) -> + AeronDriver.sessionIdAllocator.free(publication.sessionId()) + + try { + // we might not be able to close this connection. + driver.close(publication, "Server Handshake ($connectKey)") + } + catch (e: Exception) { + server.listenerManager.notifyError(e) + } + } + publications.clear() + } + } + + fun ipc(server: Server, handshake: ServerHandshake): AeronPoller { + val logger = server.logger + val config = server.config as ServerConfiguration + + val poller = try { + val driver = ServerHandshakeDriver.build( + aeronDriver = server.aeronDriver, + isIpc = true, + port = 0, + ipInfo = server.ipInfo, + streamIdSub = config.ipcId, + sessionIdSub = AeronDriver.RESERVED_SESSION_ID_INVALID, + logInfo = "HANDSHAKE-IPC" + ) + + object : AeronPoller { + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be: + // - long running + // - re-entrant with the client + val subscription = driver.subscription + + val delegate = IpcProc(logger, server, server.aeronDriver, handshake) + val handler = FragmentAssembler(delegate) override fun poll(): Int { return subscription.poll(handler, 1) } override fun close() { - subscription.close() + delegate.close() + handler.clear() + try { + driver.unsafeClose() + } + catch (ignored: Exception) { + // we are already shutting down, ignore + } + logger.info("Closed IPC poller") } - override val info = driver.info + override val info = "IPC ${driver.info}" } - } else { + } catch (e: Exception) { + server.listenerManager.notifyError(ServerException("Unable to create IPC listener.", e)) disabled("IPC Disabled") } - logger.info { poller.info } return poller } - fun ip4( - aeronDriver: AeronDriver, config: ServerConfiguration, server: Server, handshake: ServerHandshake - ): AeronPoller - { + fun ip4(server: Server, handshake: ServerHandshake): AeronPoller { val logger = server.logger - val connectionFunc = server.connectionFunc + val config = server.config val isReliable = config.isReliable - val poller = if (server.canUseIPv4) { - val driver = ServerUdpDriver( - listenAddress = server.listenIPv4Address!!, - port = config.port, - streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID, - sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID, - connectionTimeoutSec = config.connectionCloseTimeoutInSeconds, - isReliable = isReliable + val poller = try { + val driver = ServerHandshakeDriver.build( + aeronDriver = server.aeronDriver, + isIpc = false, + ipInfo = server.ipInfo, + port = server.port1, + streamIdSub = config.udpId, + sessionIdSub = 9, + logInfo = "HANDSHAKE-IPv4" ) - driver.build(aeronDriver, logger) - - val subscription = driver.subscription - object : AeronPoller { - /** - * Note: - * Reassembly has been shown to be minimal impact to latency. But not totally negligible. If the lowest latency is - * desired, then limiting message sizes to MTU size is a good practice. - * - * There is a maximum length allowed for messages which is the min of 1/8th a term length or 16MB. - * Messages larger than this should chunked using an application level chunking protocol. Chunking has better recovery - * properties from failure and streams with mechanical sympathy. - */ - val handler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header -> - ipProcessing(logger, server, isReliable, aeronDriver, false, header, buffer, offset, length, handshake, connectionFunc) - } + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be: + // - long running + // - re-entrant with the client + val subscription = driver.subscription + + val delegate = UdpProc(logger, server, server.aeronDriver, handshake, isReliable) + val handler = FragmentAssembler(delegate) override fun poll(): Int { return subscription.poll(handler, 1) } override fun close() { - subscription.close() + delegate.close() + handler.clear() + try { + driver.unsafeClose() + } + catch (ignored: Exception) { + // we are already shutting down, ignore + } + logger.info("Closed IPv4 poller") } override val info = "IPv4 ${driver.info}" } - } else { + } catch (e: Exception) { + server.listenerManager.notifyError(ServerException("Unable to create IPv4 listener.", e)) disabled("IPv4 Disabled") } - logger.info { poller.info } return poller } - fun ip6( - aeronDriver: AeronDriver, config: ServerConfiguration, server: Server, handshake: ServerHandshake - ): AeronPoller - { + fun ip6(server: Server, handshake: ServerHandshake): AeronPoller { val logger = server.logger - val connectionFunc = server.connectionFunc + val config = server.config val isReliable = config.isReliable - val poller = if (server.canUseIPv6) { - val driver = ServerUdpDriver( - listenAddress = server.listenIPv6Address!!, - port = config.port, - streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID, - sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID, - connectionTimeoutSec = config.connectionCloseTimeoutInSeconds, - isReliable = isReliable + val poller = try { + val driver = ServerHandshakeDriver.build( + aeronDriver = server.aeronDriver, + isIpc = false, + ipInfo = server.ipInfo, + port = server.port1, + streamIdSub = config.udpId, + sessionIdSub = 0, + logInfo = "HANDSHAKE-IPv6" ) - driver.build(aeronDriver, logger) - - val subscription = driver.subscription - object : AeronPoller { - /** - * Note: - * Reassembly has been shown to be minimal impact to latency. But not totally negligible. If the lowest latency is - * desired, then limiting message sizes to MTU size is a good practice. - * - * There is a maximum length allowed for messages which is the min of 1/8th a term length or 16MB. - * Messages larger than this should chunked using an application level chunking protocol. Chunking has better recovery - * properties from failure and streams with mechanical sympathy. - */ - val handler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header -> - ipProcessing(logger, server, isReliable, aeronDriver, false, header, buffer, offset, length, handshake, connectionFunc) - } + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be: + // - long running + // - re-entrant with the client + val subscription = driver.subscription + + val delegate = UdpProc(logger, server, server.aeronDriver, handshake, isReliable) + val handler = FragmentAssembler(delegate) override fun poll(): Int { return subscription.poll(handler, 1) } override fun close() { - subscription.close() + delegate.close() + handler.clear() + try { + driver.unsafeClose() + } + catch (ignored: Exception) { + // we are already shutting down, ignore + } + logger.info("Closed IPv4 poller") } override val info = "IPv6 ${driver.info}" } - } else { + } catch (e: Exception) { + server.listenerManager.notifyError(ServerException("Unable to create IPv6 listener.")) disabled("IPv6 Disabled") } - logger.info { poller.info } return poller } - fun ip6Wildcard( - aeronDriver: AeronDriver, config: ServerConfiguration, server: Server, handshake: ServerHandshake - ): AeronPoller { + fun ip6Wildcard(server: Server, handshake: ServerHandshake): AeronPoller { + val logger = server.logger - val connectionFunc = server.connectionFunc + val config = server.config val isReliable = config.isReliable - val driver = ServerUdpDriver( - listenAddress = server.listenIPv6Address!!, - port = config.port, - streamId = AeronDriver.UDP_HANDSHAKE_STREAM_ID, - sessionId = AeronDriver.RESERVED_SESSION_ID_INVALID, - connectionTimeoutSec = config.connectionCloseTimeoutInSeconds, - isReliable = isReliable - ) - - driver.build(aeronDriver, logger) - - val subscription = driver.subscription - - val poller = object : AeronPoller { - /** - * Note: - * Reassembly has been shown to be minimal impact to latency. But not totally negligible. If the lowest latency is - * desired, then limiting message sizes to MTU size is a good practice. - * - * There is a maximum length allowed for messages which is the min of 1/8th a term length or 16MB. - * Messages larger than this should chunked using an application level chunking protocol. Chunking has better recovery - * properties from failure and streams with mechanical sympathy. - */ - val handler = FragmentAssembler { buffer: DirectBuffer, offset: Int, length: Int, header: Header -> - ipProcessing(logger, server, isReliable, aeronDriver, true, header, buffer, offset, length, handshake, connectionFunc) - } + val poller = try { + val driver = ServerHandshakeDriver.build( + aeronDriver = server.aeronDriver, + isIpc = false, + ipInfo = server.ipInfo, + port = server.port1, + streamIdSub = config.udpId, + sessionIdSub = 0, + logInfo = "HANDSHAKE-IPv4+6" + ) - override fun poll(): Int { - return subscription.poll(handler, 1) - } + object : AeronPoller { + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be: + // - long running + // - re-entrant with the client + val subscription = driver.subscription - override fun close() { - subscription.close() - } + val delegate = UdpProc(logger, server, server.aeronDriver, handshake, isReliable) + val handler = FragmentAssembler(delegate) + + override fun poll(): Int { + return subscription.poll(handler, 1) + } + + override fun close() { + delegate.close() + handler.clear() + try { + driver.unsafeClose() + } + catch (ignored: Exception) { + // we are already shutting down, ignore + } + logger.info("Closed IPv4+6 poller") + } - override val info = "IPv4+6 ${driver.info}" + override val info = "IPv4+6 ${driver.info}" + } + } catch (e: Exception) { + server.listenerManager.notifyError(ServerException("Unable to create IPv4+6 listeners.", e)) + disabled("IPv4+6 Disabled") } - logger.info { poller.info } return poller } } diff --git a/src/dorkbox/network/handshake/package-info.java b/src/dorkbox/network/handshake/package-info.java new file mode 100644 index 00000000..ea49a465 --- /dev/null +++ b/src/dorkbox/network/handshake/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.handshake; diff --git a/src/dorkbox/network/ipFilter/IpSubnetFilterRule.kt b/src/dorkbox/network/ipFilter/IpSubnetFilterRule.kt index 314eeeac..bb57dd51 100644 --- a/src/dorkbox/network/ipFilter/IpSubnetFilterRule.kt +++ b/src/dorkbox/network/ipFilter/IpSubnetFilterRule.kt @@ -26,7 +26,7 @@ import java.net.InetAddress * * Supports both IPv4 and IPv6. */ -internal class IpSubnetFilterRule : IpFilterRule { +class IpSubnetFilterRule : IpFilterRule { private val filterRule: IpFilterRule constructor(ipAddress: String, cidrPrefix: Int) { diff --git a/src/dorkbox/network/ipFilter/package-info.java b/src/dorkbox/network/ipFilter/package-info.java new file mode 100644 index 00000000..8e08bbcd --- /dev/null +++ b/src/dorkbox/network/ipFilter/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.ipFilter; diff --git a/src/dorkbox/network/package-info.java b/src/dorkbox/network/package-info.java new file mode 100644 index 00000000..bd4e8b91 --- /dev/null +++ b/src/dorkbox/network/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network; diff --git a/src/dorkbox/network/ping/Ping.kt b/src/dorkbox/network/ping/Ping.kt index 3a593837..3fb9f559 100644 --- a/src/dorkbox/network/ping/Ping.kt +++ b/src/dorkbox/network/ping/Ping.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ class Ping { var packedId = 0 // ping/pong times are the LOWER 8 bytes of a long, which gives us 65 seconds. This is the same as the max value timeout (a short) so this is acceptable - var pingTime = 0L var pongTime = 0L diff --git a/src/dorkbox/network/ping/PingManager.kt b/src/dorkbox/network/ping/PingManager.kt deleted file mode 100644 index 315bac52..00000000 --- a/src/dorkbox/network/ping/PingManager.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2021 dorkbox, llc - * - * 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. - */ - - -@file:Suppress("UNUSED_PARAMETER") - -package dorkbox.network.ping - -import dorkbox.network.connection.Connection -import dorkbox.network.rmi.ResponseManager -import kotlinx.coroutines.CoroutineScope -import mu.KLogger -import java.util.concurrent.* - -/** - * How to handle ping messages - */ -internal class PingManager { - companion object { - val DEFAULT_TIMEOUT_SECONDS = 30 - } - - @Suppress("UNCHECKED_CAST") - suspend fun manage(connection: CONNECTION, responseManager: ResponseManager, ping: Ping, logger: KLogger) { - if (ping.pongTime == 0L) { - ping.pongTime = System.currentTimeMillis() - connection.send(ping) - } else { - ping.finishedTime = System.currentTimeMillis() - - val rmiId = ping.packedId - - // process the ping message so that our ping callback does something - - // this will be null if the ping took longer than XXX seconds and was cancelled - val result = responseManager.getWaiterCallback(rmiId, logger) as (suspend Ping.() -> Unit)? - if (result != null) { - result(ping) - } - } - } - - /** - * Sends a "ping" packet to measure **ROUND TRIP** time to the remote connection. - * - * @return true if the message was successfully sent by aeron - */ - internal suspend fun ping( - connection: Connection, - pingTimeoutSeconds: Int, - actionDispatch: CoroutineScope, - responseManager: ResponseManager, - logger: KLogger, - function: suspend Ping.() -> Unit - ): Boolean { - val id = responseManager.prepWithCallback(function, logger) - - val ping = Ping() - ping.packedId = id - ping.pingTime = System.currentTimeMillis() - - // ALWAYS cancel the ping after XXX seconds - responseManager.cancelRequest(actionDispatch, TimeUnit.SECONDS.toMillis(pingTimeoutSeconds.toLong()), id, logger) { - // kill the callback, since we are now "cancelled". If there is a race here (and the response comes at the exact same time) - // we don't care since either it will be null or it won't (if it's not null, it will run the callback) - result = null - } - - return connection.send(ping) - } -} diff --git a/src/dorkbox/network/ping/PingSerializer.kt b/src/dorkbox/network/ping/PingSerializer.kt index 2f765c14..5a90d7f9 100644 --- a/src/dorkbox/network/ping/PingSerializer.kt +++ b/src/dorkbox/network/ping/PingSerializer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output -class PingSerializer: Serializer() { +internal class PingSerializer: Serializer() { override fun write(kryo: Kryo, output: Output, ping: Ping) { output.writeInt(ping.packedId) output.writeLong(ping.pingTime) diff --git a/src/dorkbox/network/ping/package-info.java b/src/dorkbox/network/ping/package-info.java new file mode 100644 index 00000000..f0fe93fc --- /dev/null +++ b/src/dorkbox/network/ping/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.ping; diff --git a/src/dorkbox/network/rmi/RemoteObject.kt b/src/dorkbox/network/rmi/RemoteObject.kt index a6e5d986..fd8a7ee2 100644 --- a/src/dorkbox/network/rmi/RemoteObject.kt +++ b/src/dorkbox/network/rmi/RemoteObject.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,25 +12,6 @@ * 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. - * - * Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package dorkbox.network.rmi @@ -38,18 +19,27 @@ package dorkbox.network.rmi * Provides access to various settings on a remote object. * * @author Nathan Sweet @n4te.com> + * @author dorkbox, llc */ -interface RemoteObject { +interface RemoteObject { + companion object { + fun cast(remoteObject: T): RemoteObject { + @Suppress("UNCHECKED_CAST") + return remoteObject as RemoteObject + } + } /** - * This is the the milliseconds to wait for a method to return a value. Default is 3000, 0 disables (waits forever) + * This is the milliseconds to wait for a method to return a value. Default is 3000, 0 disables (waits forever) */ var responseTimeout: Int /** - * Sets the behavior when invoking a remote method. DEFAULT is false. This is not thread safe! + * Sets the behavior when invoking a remote method. DEFAULT is false. THIS CAN BE MODIFIED CONCURRENTLY AND IS NOT SAFE! + * This is meant to be used to set the communication state "permanently", and not to be regularly changed as this is + * not safe for concurrent use (one thread sets it, and another thread reads the correct, but unexpected value) * - * If true, the invoking thread will not wait for a response. The method will return immediately and the return value + * If true, the invoking thread will NOT WAIT for a response. The method will return immediately and the return value * should be ignored. * * If false, the invoking thread will wait for the remote method to return or timeout. @@ -58,6 +48,52 @@ interface RemoteObject { */ var async: Boolean + /** + * Sets the ASYNC behavior when invoking remote methods, for whichever remote methods are in the unit function. THIS IS CONCURRENT/THREAD SAFE! + * The primary use-case for this, is when calling the RMI methods of a different type (sync/async) than is currently configured + * for this connection via the initial setting of `async` (default is false) + * + * For these methods, invoking thread WILL NOT wait for a response. The method will return immediately and the return value + * should be ignored. + */ + fun async(action: T.() -> Unit) + + + /** + * Sets the ASYNC behavior when invoking remote methods, for whichever remote methods are in the unit function. THIS IS CONCURRENT/THREAD SAFE! + * The primary use-case for this, is when calling the RMI methods of a different type (sync/async) than is currently configured + * for this connection via the initial setting of `async` (default is false) + * + * For these methods, invoking thread WILL NOT wait for a response. The method will return immediately and the return value + * should be ignored. + */ + suspend fun asyncSuspend(action: suspend T.() -> Unit) + + + /** + * Sets the SYNC behavior when invoking remote methods, for whichever remote methods are in the unit function. THIS IS CONCURRENT/THREAD SAFE! + * The primary use-case for this, is when calling the RMI methods of a different type (sync/async) than is currently configured + * for this connection via the initial setting of `async` (default is false) + * + * For these methods, the invoking thread WILL wait for a response or timeout. + * + * If the return value or an exception needs to be retrieved, then DO NOT set async=true, and change the response timeout to 0 instead + */ + fun sync(action: T.() -> Unit) + + + /** + * Sets the SYNC behavior when invoking remote methods, for whichever remote methods are in the unit function. THIS IS CONCURRENT/THREAD SAFE! + * The primary use-case for this, is when calling the RMI methods of a different type (sync/async) than is currently configured + * for this connection via the initial setting of `async` (default is false) + * + * For these methods, the invoking thread WILL wait for a response or timeout. + * + * If the return value or an exception needs to be retrieved, then DO NOT set async=true, and change the response timeout to 0 instead + */ + suspend fun syncSuspend(action: suspend T.() -> Unit) + + /** * Permits calls to [Object.toString] to actually return the `toString()` method on the object. * diff --git a/src/dorkbox/network/rmi/RemoteObjectCallback.kt b/src/dorkbox/network/rmi/RemoteObjectCallback.kt index cab066bb..0fd80513 100644 --- a/src/dorkbox/network/rmi/RemoteObjectCallback.kt +++ b/src/dorkbox/network/rmi/RemoteObjectCallback.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,5 +23,5 @@ interface RemoteObjectCallback { /** * @param remoteObject the remote object (as a proxy object) or null if there was an error creating the RMI object */ - suspend fun created(remoteObject: Iface) + fun created(remoteObject: Iface) } diff --git a/src/dorkbox/network/rmi/RemoteObjectStorage.kt b/src/dorkbox/network/rmi/RemoteObjectStorage.kt index d06c6a80..35de8025 100644 --- a/src/dorkbox/network/rmi/RemoteObjectStorage.kt +++ b/src/dorkbox/network/rmi/RemoteObjectStorage.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package dorkbox.network.rmi import dorkbox.collections.LockFreeIntBiMap -import mu.KLogger import org.agrona.collections.IntArrayList +import org.slf4j.Logger import java.util.concurrent.locks.* import kotlin.concurrent.write @@ -59,7 +59,7 @@ import kotlin.concurrent.write * * @author Nathan Robinson */ -internal class RemoteObjectStorage(val logger: KLogger) { +class RemoteObjectStorage(val logger: Logger) { companion object { const val INVALID_RMI = 0 @@ -75,112 +75,43 @@ internal class RemoteObjectStorage(val logger: KLogger) { // there are 2 ways to get an RMI object ID // 1) request the next number from the counter // 2) specifically request a number - // To solve this, we use 3 data structures, because it's also possible to RETURN no-longer needed object ID's (like when a - // connection closes) + // To solve this, we use 3 data structures, because it's also possible to RETURN no-longer needed object ID's (like when a connection closes) private var objectIdCounter: Int = 1 - private val reservedObjectIds = IntArrayList(1, INVALID_RMI) private val objectIds = IntArrayList(16, INVALID_RMI) - init { - (0..8).forEach { _ -> - objectIds.addInt(objectIdCounter++) - } - } - private fun validate(objectId: Int) { require(objectId > 0) { "The ID must be greater than 0" } require(objectId <= 65535) { "The ID must be less than 65,535" } } - /** - * @return the next ID or 0 (INVALID_RMI, if it's invalid) - */ - private fun unsafeNextId(): Int { - val id = if (objectIds.size > 0) { - objectIds.removeAt(objectIds.size - 1) - } else { - objectIdCounter++ - } - - if (objectIdCounter > 65535) { - // basically, it's a short (but collections are a LOT easier to deal with if it's an int) - val msg = "Max ID size is 65535, because of how we pack the bytes when sending RMI messages. FATAL ERROR! (too many objects)" - logger.error(msg) - return INVALID_RMI - } - - return id - } - /** * @return the next possible RMI object ID. Either one that is next available, or 0 (INVALID_RMI) if it was invalid */ fun nextId(): Int { idLock.write { - var idToReturn = unsafeNextId() - while (reservedObjectIds.contains(idToReturn)) { - idToReturn = unsafeNextId() - } - - return idToReturn - } - } - - - /** - * Reserves an ID so that other requests for ID's will never return this ID. The number must be > 0 and < 65535 - * - * Reservations are permanent and it will ALWAYS be reserved! You cannot "un-reserve" an ID. - * - * If you care about memory and performance, use the ID from "nextId()" instead. - * - * @return false if this ID was not able to be reserved - */ - fun reserveId(id: Int): Boolean { - validate(id) - - idLock.write { - val contains = objectIds.remove(id) - if (contains) { - // this id is available for us to use (and was temporarily used before) - return true - } - - if (reservedObjectIds.contains(id)) { - // this id is ALREADY used by something else - return false - } - - if (objectIdCounter < id) { - // this id is ALREADY used by something else - return false + val id = if (objectIds.size > 0) { + objectIds.removeAt(objectIds.size - 1) + } else { + objectIdCounter++ } - if (objectIdCounter == id) { - // we are available via the counter, so make sure the counter increments - objectIdCounter++ - // we still want to mark this as reserved, so fall through + if (objectIdCounter > 65535) { + // basically, it's a short (but collections are a LOT easier to deal with if it's an int) + val msg = "Max ID size is 65535, because of how we pack the bytes when sending RMI messages. FATAL ERROR! (too many objects)" + logger.error(msg) + return INVALID_RMI } - // this means that the counter is LARGER than the id (maybe even a LOT larger) - // we just stuff this requested number in a small array and check it whenever we get a new number - reservedObjectIds.add(id) - return true + return id } } + /** * @return an ID to be used again. Reserved IDs will not be allowed to be returned */ fun returnId(id: Int) { idLock.write { - if (reservedObjectIds.contains(id)) { - logger.error { - "Do not return a reserved ID ($id). Once an ID is reserved, it is permanent." - } - return - } - val shortCheck: Int = (id + 1) if (shortCheck == objectIdCounter) { objectIdCounter-- @@ -191,9 +122,6 @@ internal class RemoteObjectStorage(val logger: KLogger) { } } - - - /** * Automatically registers an object with the next available ID to allow a remote connection to access this object via the returned ID * @@ -203,10 +131,10 @@ internal class RemoteObjectStorage(val logger: KLogger) { // this will return INVALID_RMI if there are too many in the ObjectSpace val nextObjectId = nextId() if (nextObjectId != INVALID_RMI) { - objectMap.put(nextObjectId, `object`) + objectMap[nextObjectId] = `object` - logger.trace { - "Remote object registered with .toString() = '${`object`}'" + if (logger.isTraceEnabled) { + logger.trace("Remote object registered with .toString() = '${`object`}'") } } @@ -223,10 +151,10 @@ internal class RemoteObjectStorage(val logger: KLogger) { fun register(`object`: Any, objectId: Int): Boolean { validate(objectId) - objectMap.put(objectId, `object`) + objectMap[objectId] = `object` - logger.trace { - "Remote object registered with .toString() = '${`object`}'" + if (logger.isTraceEnabled) { + logger.trace("Remote object registered with .toString() = '${`object`}'") } return true @@ -242,10 +170,13 @@ internal class RemoteObjectStorage(val logger: KLogger) { val rmiObject = objectMap.remove(objectId) as T? returnId(objectId) - logger.trace { - "Object removed" + if (logger.isTraceEnabled) { + if (rmiObject is RemoteObject<*>) { + logger.trace("Object removed") + } else { + logger.trace("Object removed") + } } - @Suppress("UNCHECKED_CAST") return rmiObject } @@ -260,8 +191,8 @@ internal class RemoteObjectStorage(val logger: KLogger) { } else { returnId(objectId) - logger.trace { - "Object '${remoteObject}' (ID: ${objectId}) removed from RMI system." + if (logger.isTraceEnabled) { + logger.trace("Object '${remoteObject}' (ID: ${objectId}) removed from RMI system.") } } } @@ -278,12 +209,42 @@ internal class RemoteObjectStorage(val logger: KLogger) { /** * @return the ID registered for the specified object, or INVALID_RMI if not found. */ - fun getId(remoteObject: T): Int { + fun getId(remoteObject: T): Int { // Find an ID with the object. return objectMap.inverse()[remoteObject] } - fun close() { + + /** + * @return all the saved objects along with their RMI ID. This is so we can restore these later on + */ + fun getAll(): List> { + return objectMap.entries.map { it -> Pair(it.key, it.value) }.toList() + } + + /** + * @return all the saved RMI implementation objects along with their RMI ID. This is so we can restore these later on + */ + fun restoreAll(implObjects: List>) { + idLock.write { + // this is a bit slow, but we have to re-inject objects. THIS happens before the connection is initialized, so we know + // these RMI ids are available + + implObjects.forEach { + objectMap.remove(it.first) + } + + objectIdCounter += implObjects.size + } + + + // now we have to put our items back into the backing map. + implObjects.forEach { + objectMap[it.first] = it.second + } + } + + fun clear() { objectMap.clear() } } diff --git a/src/dorkbox/network/rmi/ResponseManager.kt b/src/dorkbox/network/rmi/ResponseManager.kt index 2f61f317..87c27d37 100644 --- a/src/dorkbox/network/rmi/ResponseManager.kt +++ b/src/dorkbox/network/rmi/ResponseManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,90 +15,80 @@ */ package dorkbox.network.rmi +import dorkbox.objectPool.ObjectPool +import dorkbox.objectPool.Pool import kotlinx.atomicfu.atomic -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ClosedSendChannelException -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import mu.KLogger +import org.slf4j.Logger import java.util.concurrent.locks.* -import kotlin.concurrent.read import kotlin.concurrent.write /** * Manages the "pending response" from method invocation. * - * response ID's and the memory they hold will leak if the response never arrives! + * + * Response IDs are used for in-flight RMI on the network stack. and are limited to 65,535 TOTAL + * + * - these are just looped around in a ring buffer. + * - these are stored here as int, however these are REALLY shorts and are int-packed when transferring data on the wire + * + * (By default, for RMI/Ping/SendSync...) + * - 0 is reserved for INVALID + * - 1 is reserved for ASYNC (the response will never be sent back, and we don't wait for it) + * */ -internal class ResponseManager(logger: KLogger, actionDispatch: CoroutineScope) { +internal class ResponseManager(maxValuesInCache: Int = 65534, minimumValue: Int = 2) { companion object { - val TIMEOUT_EXCEPTION = Exception() + val TIMEOUT_EXCEPTION = TimeoutException().apply { stackTrace = arrayOf() } } - - // Response ID's are for ALL in-flight RMI on the network stack. instead of limited to (originally) 64, we are now limited to 65,535 - // these are just looped around in a ring buffer. - // These are stored here as int, however these are REALLY shorts and are int-packed when transferring data on the wire - // 65535 IN FLIGHT RMI method invocations is plenty - // 0 is reserved for INVALID - // 1 is reserved for ASYNC - private val maxValuesInCache = 65535 - private val rmiWaitersInUse = atomic(0) - private val waiterCache = Channel(maxValuesInCache) + private val responseWaitersInUse = atomic(0) + private val waiterCache: Pool private val pendingLock = ReentrantReadWriteLock() private val pending = arrayOfNulls(maxValuesInCache+1) // +1 because it's possible to have the value 65535 in the cache init { - // create a shuffled list of ID's. This operation is ONLY performed ONE TIME per endpoint! - val ids = mutableListOf() - - // MIN (32768) -> -1 (65535) - // ZERO is special, and is never added! - // ONE is special, and is used for ASYNC (the response will never be sent back) - // 2 (2) -> MAX (32767) + require(maxValuesInCache <= 65535) { "The maximum size for the values in the response manager is 65535"} + require(maxValuesInCache > minimumValue) { "< $minimumValue (0 and 1 for RMI/Ping/SendSync) are reserved"} + require(minimumValue > 1) { "The minimum value $minimumValue must be > 1"} + // create a shuffled list of ID's. This operation is ONLY performed ONE TIME per endpoint! + val ids = mutableListOf() - for (id in Short.MIN_VALUE..-1) { - ids.add(id) - } - for (id in 2..Short.MAX_VALUE) { - ids.add(id) + // 0 is special, and is never added! + // 1 is special, and is used for ASYNC (the response will never be sent back) + for (id in minimumValue..maxValuesInCache) { + ids.add(ResponseWaiter(id)) } ids.shuffle() - // populate the array of randomly assigned ID's + waiters. It is OK for this to happen in a new thread - actionDispatch.launch { - try { - for (it in ids) { - waiterCache.send(ResponseWaiter(it)) - } - } catch (e: ClosedSendChannelException) { - // this can happen if we are starting/stopping an endpoint (and thus a response-manager) VERY quickly, and can be ignored - logger.trace("Error during RMI preparation. Usually this is caused by fast a start-then-stop") - } - } + // populate the array of randomly assigned ID's + waiters. + waiterCache = ObjectPool.blocking(ids) } /** * Called when we receive the answer for our initial request. If no response data, then the pending rmi data entry is deleted * * resume any pending remote object method invocations (if they are not async, or not manually waiting) + * * NOTE: async RMI will never call this (because async doesn't return a response) */ - suspend fun notifyWaiter(rmiId: Int, result: Any?, logger: KLogger) { - logger.trace { "RMI return message: $rmiId" } + fun notifyWaiter(id: Int, result: Any?, logger: Logger) { + if (logger.isTraceEnabled) { + logger.trace("[RM] notify: [$id]") + } val previous = pendingLock.write { - val previous = pending[rmiId] - pending[rmiId] = result + val previous = pending[id] + pending[id] = result previous } // if NULL, since either we don't exist (because we were async), or it was cancelled if (previous is ResponseWaiter) { - logger.trace { "RMI valid-cancel onMessage: $rmiId" } + if (logger.isTraceEnabled) { + logger.trace("[RM] valid-notify: [$id]") + } // this means we were NOT timed out! (we cannot be timed out here) previous.doNotify() @@ -110,12 +100,14 @@ internal class ResponseManager(logger: KLogger, actionDispatch: CoroutineScope) * * This is ONLY called when we want to get the data out of the stored entry, because we are operating ASYNC. (pure RMI async is different) */ - suspend fun getWaiterCallback(rmiId: Int, logger: KLogger): T? { - logger.trace { "RMI return message: $rmiId" } + fun removeWaiterCallback(id: Int, logger: Logger): T? { + if (logger.isTraceEnabled) { + logger.trace("[RM] get-callback: [$id]") + } val previous = pendingLock.write { - val previous = pending[rmiId] - pending[rmiId] = null + val previous = pending[id] + pending[id] = null previous } @@ -124,8 +116,9 @@ internal class ResponseManager(logger: KLogger, actionDispatch: CoroutineScope) val result = previous.result // always return this to the cache! - waiterCache.send(previous) - rmiWaitersInUse.getAndDecrement() + previous.result = null + waiterCache.put(previous) + responseWaitersInUse.getAndDecrement() return result as T } @@ -138,22 +131,21 @@ internal class ResponseManager(logger: KLogger, actionDispatch: CoroutineScope) * * We ONLY care about the ID to get the correct response info. If there is no response, the ID can be ignored. */ - suspend fun prep(logger: KLogger): ResponseWaiter { - val responseRmi = waiterCache.receive() - rmiWaitersInUse.getAndIncrement() - logger.trace { "RMI count: ${rmiWaitersInUse.value}" } - - // this will replace the waiter if it was cancelled (waiters are not valid if cancelled) - responseRmi.prep() + fun prep(logger: Logger): ResponseWaiter { + val waiter = waiterCache.take() + responseWaitersInUse.getAndIncrement() + if (logger.isTraceEnabled) { + logger.trace("[RM] prep in-use: [${waiter.id}] ${responseWaitersInUse.value}") + } - val rmiId = RmiUtils.unpackUnsignedRight(responseRmi.id) + // this will initialize the result + waiter.prep() pendingLock.write { - // this just does a .toUShort().toInt() conversion. This is cleaner than doing it manually - pending[rmiId] = responseRmi + pending[waiter.id] = waiter } - return responseRmi + return waiter } /** @@ -161,126 +153,96 @@ internal class ResponseManager(logger: KLogger, actionDispatch: CoroutineScope) * * We ONLY care about the ID to get the correct response info. If there is no response, the ID can be ignored. */ - suspend fun prepWithCallback(function: Any, logger: KLogger): Int { - val responseRmi = waiterCache.receive() - rmiWaitersInUse.getAndIncrement() - logger.trace { "RMI count: ${rmiWaitersInUse.value}" } + fun prepWithCallback(logger: Logger, function: Any): Int { + val waiter = waiterCache.take() + responseWaitersInUse.getAndIncrement() + if (logger.isTraceEnabled) { + logger.trace("[RM] prep in-use: [${waiter.id}] ${responseWaitersInUse.value}") + } - // this will replace the waiter if it was cancelled (waiters are not valid if cancelled) - responseRmi.prep() + // this will initialize the result + waiter.prep() // assign the callback that will be notified when the return message is received - responseRmi.result = function + waiter.result = function - val rmiId = RmiUtils.unpackUnsignedRight(responseRmi.id) + val id = RmiUtils.unpackUnsignedRight(waiter.id) pendingLock.write { - pending[rmiId] = responseRmi + pending[id] = waiter } - return rmiId + return id } - /** - * Cancels the RMI request in the given timeout, the callback is executed inside the read lock - */ - fun cancelRequest(actionDispatch: CoroutineScope, timeoutMillis: Long, rmiId: Int, logger: KLogger, onCancelled: ResponseWaiter.() -> Unit) { - actionDispatch.launch { - delay(timeoutMillis) // this will always wait. if this job is cancelled, this will immediately stop waiting - - // check if we have a result or not - pendingLock.read { - val maybeResult = pending[rmiId] - if (maybeResult is ResponseWaiter) { - logger.trace { "RMI timeout ($timeoutMillis) with callback cancel: $rmiId" } - - maybeResult.cancel() - onCancelled(maybeResult) - } - } - } - } - /** * We only wait for a reply if we are SYNC. * - * ASYNC does not send a response + * ASYNC does not send a response and does not call this method * * @return the result (can be null) or timeout exception */ - suspend fun waitForReply(actionDispatch: CoroutineScope, responseWaiter: ResponseWaiter, timeoutMillis: Long, logger: KLogger): Any? { - val rmiId = RmiUtils.unpackUnsignedRight(responseWaiter.id) - - logger.trace { - "RMI waiting: $rmiId" - } - - // NOTE: we ALWAYS send a response from the remote end (except when async). - // - // 'async' -> DO NOT WAIT (no response) - // 'timeout > 0' -> WAIT w/ TIMEOUT - // 'timeout == 0' -> WAIT FOREVER - if (timeoutMillis > 0) { - val responseTimeoutJob = actionDispatch.launch { - delay(timeoutMillis) // this will always wait. if this job is cancelled, this will immediately stop waiting - - // check if we have a result or not - val maybeResult = pendingLock.read { pending[rmiId] } - if (maybeResult is ResponseWaiter) { - logger.trace { "RMI timeout ($timeoutMillis) cancel: $rmiId" } - - maybeResult.cancel() - } - } + fun getReply(responseWaiter: ResponseWaiter, timeoutMillis: Long, logger: Logger): Any? { + val id = RmiUtils.unpackUnsignedRight(responseWaiter.id) - // wait for the response. - // - // If the response is ALREADY here, the doWait() returns instantly (with result) - // if no response yet, it will suspend and either - // A) get response - // B) timeout - responseWaiter.doWait() - - // always cancel the timeout - responseTimeoutJob.cancel() - } else { - // wait for the response --- THIS WAITS FOREVER (there is no timeout)! - // - // If the response is ALREADY here, the doWait() returns instantly (with result) - // if no response yet, it will suspend and - // A) get response - responseWaiter.doWait() + if (logger.isTraceEnabled) { + logger.trace("[RM] get: [$id]") } - // deletes the entry in the map val resultOrWaiter = pendingLock.write { - val previous = pending[rmiId] - pending[rmiId] = null + val previous = pending[id] + pending[id] = null previous } // always return the waiter to the cache - waiterCache.send(responseWaiter) - rmiWaitersInUse.getAndDecrement() + responseWaiter.result = null + waiterCache.put(responseWaiter) + responseWaitersInUse.getAndDecrement() if (resultOrWaiter is ResponseWaiter) { - logger.trace { "RMI was canceled ($timeoutMillis): $rmiId" } + if (logger.isTraceEnabled) { + logger.trace("[RM] timeout cancel: [$id] ($timeoutMillis)") + } + // always throw an exception if we timeout. EVEN if the connection is closed, we want to make sure to raise awareness! return TIMEOUT_EXCEPTION } return resultOrWaiter } - suspend fun close() { - // wait for responses, or wait for timeouts! - while (rmiWaitersInUse.value > 0) { - delay(100) + fun abort(responseWaiter: ResponseWaiter, logger: Logger) { + val id = RmiUtils.unpackUnsignedRight(responseWaiter.id) + + if (logger.isTraceEnabled) { + logger.trace("[RM] abort: [$id]") } - waiterCache.close() + // deletes the entry in the map + pendingLock.write { + pending[id] = null + } + + // always return the waiter to the cache + responseWaiter.result = null + waiterCache.put(responseWaiter) + responseWaitersInUse.getAndDecrement() + } + + // This is only closed when shutting down the client/server. + fun close(logger: Logger) { + // technically, this isn't closing it, so much as it's cleaning it out + if (logger.isDebugEnabled) { + logger.debug("Closing the response manager") + } + + // wait for responses, or wait for timeouts! + while (responseWaitersInUse.value > 0) { + Thread.sleep(50) + } pendingLock.write { pending.forEachIndexed { index, _ -> diff --git a/src/dorkbox/network/rmi/ResponseWaiter.kt b/src/dorkbox/network/rmi/ResponseWaiter.kt index 8aabc70d..b4f935c9 100644 --- a/src/dorkbox/network/rmi/ResponseWaiter.kt +++ b/src/dorkbox/network/rmi/ResponseWaiter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,57 +15,73 @@ */ package dorkbox.network.rmi -import kotlinx.coroutines.channels.Channel +import kotlinx.atomicfu.locks.withLock +import java.util.concurrent.* +import java.util.concurrent.locks.* data class ResponseWaiter(val id: Int) { - // this is bi-directional waiting. The method names to not reflect this, however there is no possibility of race conditions w.r.t. waiting - // https://stackoverflow.com/questions/55421710/how-to-suspend-kotlin-coroutine-until-notified - // https://kotlinlang.org/docs/reference/coroutines/channels.html - // "receive' suspends until another coroutine invokes "send" - // and - // "send" suspends until another coroutine invokes "receive". - // - // these are wrapped in a try/catch, because cancel will cause exceptions to be thrown (which we DO NOT want) - @Volatile - var channel: Channel = Channel(Channel.RENDEZVOUS) + private val lock = ReentrantLock() + private val condition = lock.newCondition() @Volatile - var isCancelled = false + private var signalled = false // holds the RMI result or callback. This is ALWAYS accessed from within a lock (so no synchronize/volatile/etc necessary)! @Volatile var result: Any? = null /** - * this will replace the waiter if it was cancelled (waiters are not valid if cancelled) + * this will set the result to null */ fun prep() { - if (isCancelled) { - isCancelled = false - channel = Channel(0) - } + result = null + signalled = false } - suspend fun doNotify() { + /** + * Waits until another thread invokes "doWait" + */ + fun doNotify() { try { - channel.send(Unit) + lock.withLock { + signalled = true + condition.signal() + } } catch (ignored: Throwable) { } } - suspend fun doWait() { + /** + * Waits a specific amount of time until another thread invokes "doNotify" + */ + fun doWait() { try { - channel.receive() + lock.withLock { + if (signalled) { + return + } + condition.await() + } } catch (ignored: Throwable) { } } - fun cancel() { - try { - isCancelled = true - channel.cancel() + /** + * Waits a specific amount of time until another thread invokes "doNotify" + */ + fun doWait(timeoutMs: Long): Boolean { + return try { + lock.withLock { + if (signalled) { + true + } else { + condition.await(timeoutMs, TimeUnit.MILLISECONDS) + } + } } catch (ignored: Throwable) { + // we were interrupted BEFORE the timeout, so technically, the timeout did not elapse. + true } } } diff --git a/src/dorkbox/network/rmi/RmiClient.kt b/src/dorkbox/network/rmi/RmiClient.kt index 096352fc..c68808f9 100644 --- a/src/dorkbox/network/rmi/RmiClient.kt +++ b/src/dorkbox/network/rmi/RmiClient.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,16 +15,20 @@ */ package dorkbox.network.rmi +import com.conversantmedia.util.collection.FixedStack import dorkbox.network.connection.Connection +import dorkbox.network.connection.EndPoint +import dorkbox.network.rmi.ResponseManager.Companion.TIMEOUT_EXCEPTION import dorkbox.network.rmi.messages.MethodRequest -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asContextElement import kotlinx.coroutines.runBlocking -import mu.KLogger +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield import java.lang.reflect.InvocationHandler import java.lang.reflect.Method import java.util.* +import java.util.concurrent.* import kotlin.coroutines.Continuation -import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -59,6 +63,10 @@ internal class RmiClient(val isGlobal: Boolean, private val enableHashCodeMethod = methods.find { it.name == "enableHashCode" } private val enableEqualsMethod = methods.find { it.name == "enableEquals" } + private val asyncMethod = methods.find { it.name == "async" } + private val syncMethod = methods.find { it.name == "sync" } + private val asyncSuspendMethod = methods.find { it.name == "asyncSuspend" } + private val syncSuspendMethod = methods.find { it.name == "syncSuspend" } private val setResponseTimeoutMethod = methods.find { it.name == "setResponseTimeout" } private val getResponseTimeoutMethod = methods.find { it.name == "getResponseTimeout" } @@ -67,121 +75,156 @@ internal class RmiClient(val isGlobal: Boolean, @Suppress("UNCHECKED_CAST") private val EMPTY_ARRAY: Array = Collections.EMPTY_LIST.toTypedArray() as Array - } - private var timeoutMillis: Long = 3_000L - private var isAsync = false + private val safeAsyncStack: ThreadLocal> = ThreadLocal.withInitial { + FixedStack(64) + } - private var enableToString = false - private var enableHashCode = false - private var enableEquals = false + private const val charPrim = 0.toChar() + private const val shortPrim = 0.toShort() + private const val bytePrim = 0.toByte() - // if we are ASYNC, then this method immediately returns - private suspend fun sendRequest(actionDispatch: CoroutineScope, invokeMethod: MethodRequest, logger: KLogger): Any? { - // there is a STRANGE problem, where if we DO NOT respond/reply to method invocation, and immediate invoke multiple methods -- - // the "server" side can have out-of-order method invocation. There are 2 ways to solve this - // 1) make the "server" side single threaded - // 2) make the "client" side wait for execution response (from the "server"). <--- this is what we are using. - // - // Because we have to ALWAYS make the client wait (unless 'isAsync' is true), we will always be returning, and will always have a - // response (even if it is a void response). This simplifies our response mask, and lets us use more bits for storing the - // response ID + @Suppress("UNCHECKED_CAST") + private fun syncMethodAction(isAsync: Boolean, proxy: RemoteObject<*>, args: Array) { + val action = args[0] as Any.() -> Unit + + // the sync state is treated as a stack. Manually changing the state via `.async` field setter can cause problems, but + // the docs cover that (and say, `don't do this`) + safeAsyncStack.get().push(isAsync) + + // the `sync` method is always a unit function - we want to execute that unit function directly - this way we can control + // exactly how sync state is preserved. + try { + action(proxy) + } finally { + safeAsyncStack.get().pop() + } + } - // NOTE: we ALWAYS send a response from the remote end (except when async). - // - // 'async' -> DO NOT WAIT (no response) - // 'timeout > 0' -> WAIT w/ TIMEOUT - // 'timeout == 0' -> WAIT FOREVER + @Suppress("UNCHECKED_CAST") + private fun syncSuspendMethodAction(isAsync: Boolean, proxy: RemoteObject<*>, args: Array): Any? { + val action = args[0] as suspend Any.() -> Unit - invokeMethod.isGlobal = isGlobal + // if a 'suspend' function is called, then our last argument is a 'Continuation' object + // We will use this for our coroutine context instead of running on a new coroutine + val suspendCoroutineArg = args.last() - return if (isAsync) { - // If we are async, we ignore the response (don't invoke the response manager at all).... - invokeMethod.packedId = RmiUtils.packShorts(rmiObjectId, RemoteObjectStorage.ASYNC_RMI) + val continuation = suspendCoroutineArg as Continuation + val suspendFunction: suspend () -> Any? = { + // the sync state is treated as a stack. Manually changing the state via `.async` field setter can cause problems, but + // the docs cover that (and say, `don't do this`) + withContext(safeAsyncStack.asContextElement()) { + yield() // must have an actually suspending call here! + safeAsyncStack.get().push(isAsync) + action(proxy) + } + } - connection.send(invokeMethod) - null - } else { - // The response, even if there is NOT one (ie: not void) will always return a thing (so our code execution is in lockstep - val rmiWaiter = responseManager.prep(logger) - invokeMethod.packedId = RmiUtils.packShorts(rmiObjectId, rmiWaiter.id) + // function suspension works differently !! + val result = (suspendFunction as Function1, Any?>).invoke( + Continuation(continuation.context) { + val any = try { + it.getOrNull() + } finally { + safeAsyncStack.get().pop() + } + when (any) { + is Exception -> { + // for co-routines, it's impossible to get a legit stacktrace without impacting general performance, + // so we just don't do it. + // RmiUtils.cleanStackTraceForProxy(Exception(), any) + continuation.resumeWithException(any) + } + else -> { + continuation.resume(null) + } + } + }) - connection.send(invokeMethod) + runBlocking(safeAsyncStack.asContextElement()) {} - responseManager.waitForReply(actionDispatch, rmiWaiter, timeoutMillis, logger) + return result } } - private fun returnAsyncOrSync(method: Method, returnValue: Any?): Any? { - if (isAsync) { - // if we are async then we return immediately. - // If you want the response value, disable async! - val returnType = method.returnType - if (returnType.isPrimitive) { - return when (returnType) { - Boolean::class.javaPrimitiveType -> java.lang.Boolean.FALSE - Int::class.javaPrimitiveType -> 0 - Float::class.javaPrimitiveType -> 0.0f - Char::class.javaPrimitiveType -> 0.toChar() - Long::class.javaPrimitiveType -> 0L - Short::class.javaPrimitiveType -> 0.toShort() - Byte::class.javaPrimitiveType -> 0.toByte() - Double::class.javaPrimitiveType -> 0.0 - else -> null - } - } - return null - } - else { - return returnValue - } - } + @Volatile private var isAsync = false + @Volatile private var timeoutMillis: Long = if (EndPoint.DEBUG_CONNECTIONS) TimeUnit.HOURS.toMillis(2) else 3_000L + @Volatile private var enableToString = false + @Volatile private var enableHashCode = false + @Volatile private var enableEquals = false - @Suppress("DuplicatedCode") + @Suppress("DuplicatedCode", "UNCHECKED_CAST") /** * @throws Exception */ override fun invoke(proxy: Any, method: Method, args: Array?): Any? { + val localAsync = + safeAsyncStack.get().peek() // value set via obj.sync {} + ?: + isAsync // the value was set via obj.sync = xyz + + if (method.declaringClass == RemoteObject::class.java) { - // manage all of the RemoteObject proxy methods + // manage all the RemoteObject proxy methods when (method) { setResponseTimeoutMethod -> { timeoutMillis = (args!![0] as Int).toLong() - require(timeoutMillis >= 0) { "ResponseTimeout must be >= 0"} + require(timeoutMillis >= 0) { "ResponseTimeout must be >= 0" } return null } + getResponseTimeoutMethod -> { return timeoutMillis.toInt() } getAsyncMethod -> { - return isAsync + return localAsync } + setAsyncMethod -> { isAsync = args!![0] as Boolean return null } + + asyncMethod -> { + syncMethodAction(true, proxy as RemoteObject<*>, args!!) + return null + } + syncMethod -> { + syncMethodAction(false, proxy as RemoteObject<*>, args!!) + return null + } + + asyncSuspendMethod -> { + return syncSuspendMethodAction(true, proxy as RemoteObject<*>, args!!) + } + syncSuspendMethod -> { + return syncSuspendMethodAction(false, proxy as RemoteObject<*>, args!!) + } + enableToStringMethod -> { enableToString = args!![0] as Boolean return null } + enableHashCodeMethod -> { enableHashCode = args!![0] as Boolean return null } + enableEqualsMethod -> { enableEquals = args!![0] as Boolean return null } - else -> throw Exception("Invocation handler could not find RemoteObject method for ${method.name}") + else -> throw RmiException("Invocation handler could not find RemoteObject method for ${method.name}") } - } else { + } else { when (method) { toStringMethod -> if (!enableToString) return proxyString // otherwise, the RMI round trip logic is done for toString() hashCodeMethod -> if (!enableHashCode) return rmiObjectId // otherwise, the RMI round trip logic is done for hashCode() - equalsMethod -> { + equalsMethod -> { val other = args!![0] if (other !is RmiClient) { return false @@ -196,6 +239,8 @@ internal class RmiClient(val isGlobal: Boolean, } } + val connection = connection + // setup the RMI request val invokeMethod = MethodRequest() @@ -206,6 +251,66 @@ internal class RmiClient(val isGlobal: Boolean, // this should be accessed via the KRYO class ID + method index (both are SHORT, and can be packed) invokeMethod.cachedMethod = cachedMethods.first { it.method == method } + // there is a STRANGE problem, where if we DO NOT respond/reply to method invocation, and immediate invoke multiple methods -- + // the "server" side can have out-of-order method invocation. There are 2 ways to solve this + // 1) make the "server" side single threaded + // 2) make the "client" side wait for execution response (from the "server"). <--- this is what we are using. + // + // Because we have to ALWAYS make the client wait (unless 'isAsync' is true), we will always be returning, and will always have a + // response (even if it is a void response). This simplifies our response mask, and lets us use more bits for storing the + // response ID + + // NOTE: we ALWAYS send a response from the remote end (except when async). + // + // 'async' -> DO NOT WAIT (no response) + // 'timeout > 0' -> WAIT w/ TIMEOUT + // 'timeout == 0' -> WAIT FOREVER + + invokeMethod.isGlobal = isGlobal + + if (localAsync) { + // If we are async, we ignore the response (don't invoke the response manager at all).... + invokeMethod.packedId = RmiUtils.packShorts(rmiObjectId, RemoteObjectStorage.ASYNC_RMI) + + val success = connection.send(invokeMethod) + if (!success) { + throw RmiException("Unable to send async message, an error occurred during the send process") + } + + // if we are async then we return immediately (but must return the correct type!) + // If you want the response value, disable async! + val returnType = method.returnType + if (returnType.isPrimitive) { + return when (returnType) { + Boolean::class.javaPrimitiveType -> java.lang.Boolean.FALSE + Int::class.javaPrimitiveType -> 0 + Float::class.javaPrimitiveType -> 0.0f + Char::class.javaPrimitiveType -> charPrim + Long::class.javaPrimitiveType -> 0L + Short::class.javaPrimitiveType -> shortPrim + Byte::class.javaPrimitiveType -> bytePrim + Double::class.javaPrimitiveType -> 0.0 + else -> null // void type + } + } + return null + } + + val logger = connection.logger + + // + // this is all SYNC code + // + + // The response, even if there is NOT one (ie: not void) will always return a thing (so our code execution is in lockstep -- unless it is ASYNC) + val responseWaiter = responseManager.prep(logger) + invokeMethod.packedId = RmiUtils.packShorts(rmiObjectId, responseWaiter.id) + + val success = connection.send(invokeMethod) + if (!success) { + responseManager.abort(responseWaiter, logger) + throw RmiException("Unable to send message, an error occurred during the send process") + } // if a 'suspend' function is called, then our last argument is a 'Continuation' object @@ -214,56 +319,118 @@ internal class RmiClient(val isGlobal: Boolean, // async will return immediately if (suspendCoroutineArg is Continuation<*>) { - @Suppress("UNCHECKED_CAST") val continuation = suspendCoroutineArg as Continuation val suspendFunction: suspend () -> Any? = { - sendRequest(connection.endPoint.actionDispatch, invokeMethod, connection.logger) + // NOTE: once something ELSE is suspending, we can remove the `yield` + yield() // if this is not here, it will not work (something must actually suspend!) + + // NOTE: this is blocking! + // NOTE: we ALWAYS send a response from the remote end (except when async). + // + // 'async' -> DO NOT WAIT (no response) + // 'timeout > 0' -> WAIT w/ TIMEOUT + // 'timeout == 0' -> WAIT FOREVER + if (timeoutMillis > 0) { + // wait for the response. + // + // If the response is ALREADY here, the doWait() returns instantly (with result) + // if no response yet, it will wait for: + // A) get response + // B) timeout + if (!responseWaiter.doWait(timeoutMillis)) { + // if we timeout, it doesn't matter since we'll be removing the waiter from the array anyways, + // so no signal can occur, or a signal won't matter + responseManager.abort(responseWaiter, logger) + TIMEOUT_EXCEPTION + } else { + responseManager.getReply(responseWaiter, timeoutMillis, logger) + } + + } else { + // wait for the response --- THIS WAITS FOREVER (there is no timeout)! + // + // If the response is ALREADY here, the doWait() returns instantly (with result) + // if no response yet, it will wait for one + // A) get response + responseWaiter.doWait() + responseManager.getReply(responseWaiter, timeoutMillis, logger) + } } - // function suspension works differently !! - @Suppress("UNCHECKED_CAST") - return (suspendFunction as Function1, Any?>).invoke(Continuation(EmptyCoroutineContext) { - val any = it.getOrNull() - when (any) { - ResponseManager.TIMEOUT_EXCEPTION -> { - val fancyName = RmiUtils.makeFancyMethodName(method) - val exception = TimeoutException("Response timed out: $fancyName") - // from top down, clean up the coroutine stack - RmiUtils.cleanStackTraceForProxy(exception) - continuation.resumeWithException(exception) - } - is Exception -> { - // for co-routines, it's impossible to get a legit stacktrace without impacting general performance, - // so we just don't do it. - // RmiUtils.cleanStackTraceForProxy(Exception(), any) - continuation.resumeWithException(any) + // function suspension works differently. THIS IS A TRAMPOLINE TO CALL SUSPEND !! + return (suspendFunction as Function1, Any?>).invoke(Continuation(continuation.context) { + val any = it.getOrNull() + when (any) { + TIMEOUT_EXCEPTION -> { + val fancyName = RmiUtils.makeFancyMethodName(method) + val exception = TimeoutException("Response timed out: $fancyName") + // from top down, clean up the coroutine stack + RmiUtils.cleanStackTraceForProxy(exception) + continuation.resumeWithException(exception) + } + + is Throwable -> { + // for co-routines, it's impossible to get a legit stacktrace without impacting general performance, + // so we just don't do it. + // RmiUtils.cleanStackTraceForProxy(Exception(), any) + continuation.resumeWithException(any) + } + + else -> { + continuation.resume(any) + } } - else -> { - continuation.resume(returnAsyncOrSync(method, any)) - } - } - }) + }) } else { - val any = runBlocking { - sendRequest(connection.endPoint.actionDispatch, invokeMethod, connection.logger) + // NOTE: this is blocking! + // NOTE: we ALWAYS send a response from the remote end (except when async). + // + // 'async' -> DO NOT WAIT (no response) + // 'timeout > 0' -> WAIT w/ TIMEOUT + // 'timeout == 0' -> WAIT FOREVER + if (timeoutMillis > 0) { + // wait for the response. + // + // If the response is ALREADY here, the doWait() returns instantly (with result) + // if no response yet, it will wait for: + // A) get response + // B) timeout + if (!responseWaiter.doWait(timeoutMillis)) { + // if we timeout, it doesn't matter since we'll be removing the waiter from the array anyways, + // so no signal can occur, or a signal won't matter + responseManager.abort(responseWaiter, logger) + throw TIMEOUT_EXCEPTION + } + + } else { + // wait for the response --- THIS WAITS FOREVER (there is no timeout)! + // + // If the response is ALREADY here, the doWait() returns instantly (with result) + // if no response yet, it will wait for one + // A) get response + responseWaiter.doWait() } + + val any = responseManager.getReply(responseWaiter, timeoutMillis, logger) when (any) { - ResponseManager.TIMEOUT_EXCEPTION -> { + TIMEOUT_EXCEPTION -> { val fancyName = RmiUtils.makeFancyMethodName(method) val exception = TimeoutException("Response timed out: $fancyName") // from top down, clean up the coroutine stack RmiUtils.cleanStackTraceForProxy(exception) throw exception } - is Exception -> { + + is Throwable -> { // reconstruct the stack trace, so the calling method knows where the method invocation happened, and can trace the call // this stack will ALWAYS run up to this method (so we remove from the top->down, to get to the call site) RmiUtils.cleanStackTraceForProxy(Exception(), any) throw any } + else -> { - return returnAsyncOrSync(method, any) + return any } } } diff --git a/src/dorkbox/network/rmi/RmiException.kt b/src/dorkbox/network/rmi/RmiException.kt new file mode 100644 index 00000000..3e4e0bd8 --- /dev/null +++ b/src/dorkbox/network/rmi/RmiException.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.rmi + +/** + * Thrown when there is a generic RMI error (for example, if the RMI message could not be sent, or there is an action on an RMI object that is invalid + */ +class RmiException : Exception { + constructor() : super() {} + constructor(message: String?, cause: Throwable?) : super(message, cause) {} + constructor(message: String?) : super(message) {} + constructor(cause: Throwable?) : super(cause) {} +} diff --git a/src/dorkbox/network/rmi/RmiManagerConnections.kt b/src/dorkbox/network/rmi/RmiManagerConnections.kt index af592657..8ed30d76 100644 --- a/src/dorkbox/network/rmi/RmiManagerConnections.kt +++ b/src/dorkbox/network/rmi/RmiManagerConnections.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,19 @@ */ package dorkbox.network.rmi +import dorkbox.classUtil.ClassHelper import dorkbox.network.connection.Connection import dorkbox.network.connection.ListenerManager +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace +import dorkbox.network.exceptions.RMIException import dorkbox.network.rmi.messages.ConnectionObjectCreateRequest import dorkbox.network.rmi.messages.ConnectionObjectCreateResponse import dorkbox.network.rmi.messages.ConnectionObjectDeleteRequest -import dorkbox.network.rmi.messages.ConnectionObjectDeleteResponse import dorkbox.network.serialization.Serialization -import dorkbox.util.classes.ClassHelper -import mu.KLogger +import org.slf4j.Logger class RmiManagerConnections internal constructor( - private val logger: KLogger, + private val logger: Logger, private val responseManager: ResponseManager, private val listenerManager: ListenerManager, private val serialization: Serialization, @@ -47,20 +48,25 @@ class RmiManagerConnections internal constructor( val response = if (implObject is Exception) { // whoops! - ListenerManager.cleanStackTrace(implObject) - logger.error("RMI error connection ${connection.id}", implObject) - listenerManager.notifyError(connection, implObject) + implObject.cleanStackTrace() + val newException = RMIException(implObject) + listenerManager.notifyError(connection, newException) ConnectionObjectCreateResponse(RmiUtils.packShorts(callbackId, RemoteObjectStorage.INVALID_RMI)) } else { - val rmiId = connection.rmi.saveImplObject(implObject) - if (rmiId == RemoteObjectStorage.INVALID_RMI) { - val exception = NullPointerException("Trying to create an RMI object with the INVALID_RMI id!!") - ListenerManager.cleanStackTrace(exception) - logger.error("RMI error connection ${connection.id}", exception) - listenerManager.notifyError(connection, exception) + try { + val rmiId = connection.rmi.saveImplObject(implObject) + if (rmiId == RemoteObjectStorage.INVALID_RMI) { + val newException = RMIException("Unable to create RMI object, invalid RMI ID") + listenerManager.notifyError(connection, newException) + } + + ConnectionObjectCreateResponse(RmiUtils.packShorts(callbackId, rmiId)) + } + catch (e: Exception) { + val newException = RMIException("Error saving the RMI implementation object!", e) + listenerManager.notifyError(connection, newException) + ConnectionObjectCreateResponse(RmiUtils.packShorts(callbackId, RemoteObjectStorage.INVALID_RMI)) } - - ConnectionObjectCreateResponse(RmiUtils.packShorts(callbackId, rmiId)) } // we send the message ALWAYS, because the client needs to know it worked or not @@ -70,16 +76,14 @@ class RmiManagerConnections internal constructor( /** * called on "client" */ - suspend fun onConnectionObjectCreateResponse(connection: CONNECTION, message: ConnectionObjectCreateResponse) { + fun onConnectionObjectCreateResponse(connection: CONNECTION, message: ConnectionObjectCreateResponse) { val callbackId = RmiUtils.unpackLeft(message.packedIds) val rmiId = RmiUtils.unpackRight(message.packedIds) // we only create the proxy + execute the callback if the RMI id is valid! if (rmiId == RemoteObjectStorage.INVALID_RMI) { - val exception = Exception("RMI ID '${rmiId}' is invalid. Unable to create RMI object on server.") - ListenerManager.cleanStackTrace(exception) - logger.error("RMI error connection ${connection.id}", exception) - listenerManager.notifyError(connection, exception) + val newException = RMIException("Unable to create RMI object, invalid RMI ID") + listenerManager.notifyError(connection, newException) return } @@ -87,18 +91,17 @@ class RmiManagerConnections internal constructor( val rmi = connection.rmi as RmiSupportConnection val callback = rmi.removeCallback(callbackId) - val interfaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(RemoteObjectCallback::class.java, callback.javaClass, 0) + val interfaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(RemoteObjectCallback::class.java, callback.javaClass, 0) ?: callback.javaClass // create the client-side proxy object, if possible. This MUST be an object that is saved for the connection val proxyObject = rmi.getProxyObject(false, connection, rmiId, interfaceClass) - // this should be executed on a NEW coroutine! try { - callback(proxyObject) - } catch (e: Exception) { - ListenerManager.cleanStackTrace(e) - logger.error("RMI error connection ${connection.id}", e) - listenerManager.notifyError(connection, e) + callback(proxyObject, rmiId) + } catch (exception: Throwable) { + exception.cleanStackTrace() + val newException = RMIException(exception) + listenerManager.notifyError(connection, newException) } } @@ -110,34 +113,8 @@ class RmiManagerConnections internal constructor( // we only delete the impl object if the RMI id is valid! if (rmiId == RemoteObjectStorage.INVALID_RMI) { - val exception = Exception("RMI ID '${rmiId}' is invalid. Unable to delete RMI object!") - ListenerManager.cleanStackTrace(exception) - logger.error("RMI error connection ${connection.id}", exception) - listenerManager.notifyError(connection, exception) - return - } - - // it DOESN'T matter which "side" we are, just delete both (RMI id's must always represent the same object on both sides) - connection.rmi.removeProxyObject(rmiId) - connection.rmi.removeImplObject(rmiId) - - // tell the "other side" to delete the proxy/impl object - connection.send(ConnectionObjectDeleteResponse(rmiId)) - } - - - /** - * called on "client" or "server" - */ - fun onConnectionObjectDeleteResponse(connection: CONNECTION, message: ConnectionObjectDeleteResponse) { - val rmiId = message.rmiId - - // we only create the proxy + execute the callback if the RMI id is valid! - if (rmiId == RemoteObjectStorage.INVALID_RMI) { - val exception = Exception("RMI ID '${rmiId}' is invalid. Unable to create RMI object on server.") - ListenerManager.cleanStackTrace(exception) - logger.error("RMI error connection ${connection.id}", exception) - listenerManager.notifyError(connection, exception) + val newException = RMIException("Unable to delete RMI object, invalid RMI ID") + listenerManager.notifyError(connection, newException) return } @@ -155,7 +132,7 @@ class RmiManagerConnections internal constructor( * Methods supporting Remote Method Invocation and Objects. A new one is created for each connection (because the connection is different for each one) */ fun getNewRmiSupport(connection: Connection): RmiSupportConnection { - @Suppress("LeakingThis", "UNCHECKED_CAST") + @Suppress("UNCHECKED_CAST") return RmiSupportConnection(logger, connection as CONNECTION, responseManager, serialization, getGlobalAction) } } diff --git a/src/dorkbox/network/rmi/RmiManagerGlobal.kt b/src/dorkbox/network/rmi/RmiManagerGlobal.kt index cecace40..07157414 100644 --- a/src/dorkbox/network/rmi/RmiManagerGlobal.kt +++ b/src/dorkbox/network/rmi/RmiManagerGlobal.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,14 @@ package dorkbox.network.rmi import dorkbox.network.connection.Connection -import dorkbox.network.rmi.messages.ConnectionObjectCreateRequest -import dorkbox.network.rmi.messages.ConnectionObjectCreateResponse -import dorkbox.network.rmi.messages.ConnectionObjectDeleteRequest -import dorkbox.network.rmi.messages.ConnectionObjectDeleteResponse -import dorkbox.network.rmi.messages.MethodRequest -import dorkbox.network.rmi.messages.MethodResponse +import dorkbox.network.rmi.messages.* import dorkbox.network.serialization.Serialization -import mu.KLogger +import kotlinx.coroutines.runBlocking +import org.slf4j.Logger import java.lang.reflect.Proxy import java.util.* -internal class RmiManagerGlobal(logger: KLogger) : RmiObjectCache(logger) { +internal class RmiManagerGlobal(logger: Logger) : RmiObjectCache(logger) { companion object { /** @@ -45,15 +41,15 @@ internal class RmiManagerGlobal(logger: KLogger) : RmiOb * @param rmiId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID * @param interfaceClass this is the RMI interface class */ - internal fun createProxyObject( + internal fun createProxyObject( isGlobalObject: Boolean, connection: CONNECTION, serialization: Serialization, responseManager: ResponseManager, kryoId: Int, rmiId: Int, - interfaceClass: Class<*> - ): RemoteObject { + interfaceClass: Class + ): RemoteObject { // duplicates are fine, as they represent the same object (as specified by the ID) on the remote side. @@ -69,7 +65,8 @@ internal class RmiManagerGlobal(logger: KLogger) : RmiOb // This is the interface inheritance by the proxy object val interfaces: Array> = arrayOf(RemoteObject::class.java, interfaceClass) - return Proxy.newProxyInstance(RmiManagerGlobal::class.java.classLoader, interfaces, proxyObject) as RemoteObject + @Suppress("UNCHECKED_CAST") + return Proxy.newProxyInstance(RmiManagerGlobal::class.java.classLoader, interfaces, proxyObject) as RemoteObject } } @@ -92,13 +89,13 @@ internal class RmiManagerGlobal(logger: KLogger) : RmiOb * Manages ALL OF THE RMI SCOPES */ @Suppress("DuplicatedCode") - suspend fun processMessage( + fun processMessage( serialization: Serialization, connection: CONNECTION, message: Any, rmiConnectionSupport: RmiManagerConnections, responseManager: ResponseManager, - logger: KLogger + logger: Logger ) { when (message) { is ConnectionObjectCreateRequest -> { @@ -119,12 +116,6 @@ internal class RmiManagerGlobal(logger: KLogger) : RmiOb */ rmiConnectionSupport.onConnectionObjectDeleteRequest(connection, message) } - is ConnectionObjectDeleteResponse -> { - /** - * called on "client" or "server" - */ - rmiConnectionSupport.onConnectionObjectDeleteResponse(connection, message) - } is MethodRequest -> { /** * Invokes the method on the object and, sends the result back to the connection that made the invocation request. @@ -141,7 +132,9 @@ internal class RmiManagerGlobal(logger: KLogger) : RmiOb val args = message.args val sendResponse = rmiId != RemoteObjectStorage.ASYNC_RMI // async is always with a '1', and we should NOT send a message back if it is '1' - logger.trace { "RMI received: $rmiId" } + if (logger.isTraceEnabled) { + logger.trace("RMI received: $rmiId") + } val implObject: Any? = if (isGlobal) { getImplObject(rmiObjectId) @@ -163,10 +156,18 @@ internal class RmiManagerGlobal(logger: KLogger) : RmiOb return } - logger.trace { + if (logger.isTraceEnabled) { var argString = "" - if (args != null) { - argString = Arrays.deepToString(args) + if (!args.isNullOrEmpty()) { + // long byte arrays have SERIOUS problems! + argString = Arrays.deepToString(args.map { + when (it) { + is ByteArray -> { "${it::class.java.simpleName}(length=${it.size})"} + is Array<*> -> { "${it::class.java.simpleName}(length=${it.size})"} + is Collection<*> -> { "${it::class.java.simpleName}(length=${it.size})"} + else -> { it } + } + }.toTypedArray()) argString = argString.substring(1, argString.length - 1) } @@ -180,7 +181,9 @@ internal class RmiManagerGlobal(logger: KLogger) : RmiOb // did we override our cached method? THIS IS NOT COMMON. stringBuilder.append(" [Connection method override]") } - stringBuilder.toString() + + + logger.trace(stringBuilder.toString()) } var result: Any? @@ -188,47 +191,48 @@ internal class RmiManagerGlobal(logger: KLogger) : RmiOb if (isCoroutine) { // https://stackoverflow.com/questions/47654537/how-to-run-suspend-method-via-reflection // https://discuss.kotlinlang.org/t/calling-coroutines-suspend-functions-via-reflection/4672 - var suspendResult = kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn { cont -> - // if we are a coroutine, we have to replace the LAST arg with the coroutine object - // we KNOW this is OK, because a continuation arg will always be there! - args!![args.size - 1] = cont - - var insideResult: Any? - try { - // args!! is safe to do here (even though it doesn't make sense) - insideResult = cachedMethod.invoke(connection, implObject, args) - } catch (ex: Exception) { - insideResult = ex.cause - // added to prevent a stack overflow when references is false, (because 'cause' == "this"). - // See: - // https://groups.google.com/forum/?fromgroups=#!topic/kryo-users/6PDs71M1e9Y - if (insideResult == null) { - insideResult = ex - } - else { - insideResult.initCause(null) + runBlocking { + var suspendResult = kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn { cont -> + // if we are a coroutine, we have to replace the LAST arg with the coroutine object + // we KNOW this is OK, because a continuation arg will always be there! + args!![args.size - 1] = cont + + var insideResult: Any? + try { + insideResult = cachedMethod.invoke(connection, implObject, args) + } catch (ex: Throwable) { + insideResult = ex.cause + // added to prevent a stack overflow when references is false, (because 'cause' == "this"). + // See: + // https://groups.google.com/forum/?fromgroups=#!topic/kryo-users/6PDs71M1e9Y + if (insideResult == null) { + insideResult = ex + } + else { + insideResult.initCause(null) + } } + insideResult } - insideResult - } - if (suspendResult === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) { - // we were suspending, and the stack will resume when possible, then it will call the response below - } - else { - if (suspendResult === Unit) { - // kotlin suspend returns, that DO NOT have a return value, REALLY return kotlin.Unit. This means there is no - // return value! - suspendResult = null - } else if (suspendResult is Exception) { - RmiUtils.cleanStackTraceForImpl(suspendResult, true) - logger.error("Connection ${connection.id}", suspendResult) + if (suspendResult === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) { + // we were suspending, and the stack will resume when possible, then it will call the response below } + else { + if (suspendResult === Unit) { + // kotlin suspend returns, that DO NOT have a return value, REALLY return kotlin.Unit. This means there is no + // return value! + suspendResult = null + } else if (suspendResult is Throwable) { + RmiUtils.cleanStackTraceForImpl(suspendResult, true) + logger.error("Connection ${connection.id}", suspendResult) + } - if (sendResponse) { - val rmiMessage = returnRmiMessage(message, suspendResult, logger) - connection.send(rmiMessage) + if (sendResponse) { + val rmiMessage = returnRmiMessage(message, suspendResult, logger) + connection.send(rmiMessage) + } } } } @@ -270,8 +274,10 @@ internal class RmiManagerGlobal(logger: KLogger) : RmiOb } } - private fun returnRmiMessage(message: MethodRequest, result: Any?, logger: KLogger): MethodResponse { - logger.trace { "RMI return. Send: ${RmiUtils.unpackUnsignedRight(message.packedId)}" } + private fun returnRmiMessage(message: MethodRequest, result: Any?, logger: Logger): MethodResponse { + if (logger.isTraceEnabled) { + logger.trace("RMI return. Send: ${RmiUtils.unpackUnsignedRight(message.packedId)}") + } val rmiMessage = MethodResponse() rmiMessage.packedId = message.packedId diff --git a/src/dorkbox/network/rmi/RmiObjectCache.kt b/src/dorkbox/network/rmi/RmiObjectCache.kt index eeb9afaa..29530d55 100644 --- a/src/dorkbox/network/rmi/RmiObjectCache.kt +++ b/src/dorkbox/network/rmi/RmiObjectCache.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ */ package dorkbox.network.rmi -import mu.KLogger +import org.slf4j.Logger /** * Cache for implementation and proxy objects. @@ -23,11 +23,15 @@ import mu.KLogger * The impl/proxy objects CANNOT be stored in the same data structure, because their IDs are not tied to the same ID source (and there * would be conflicts in the data structure) */ -open class RmiObjectCache(logger: KLogger) { +open class RmiObjectCache(val logger: Logger) { private val implObjects = RemoteObjectStorage(logger) /** + * This object will be saved again if we send the object "over the wire", automatically! + * + * So if we DELETE the object (on side A), and then later on side A sends the object to side B, then side A will save it again when it sends. + * * @return the newly registered RMI ID for this object. [RemoteObjectStorage.INVALID_RMI] means it was invalid (an error log will be emitted) */ internal fun saveImplObject(rmiObject: Any): Int { @@ -50,7 +54,7 @@ open class RmiObjectCache(logger: KLogger) { } /** - * Removes the object using the ID registered. + * Removes the object using the registered ID. * * @return the object or null if not found */ @@ -61,7 +65,26 @@ open class RmiObjectCache(logger: KLogger) { /** * @return the ID registered for the specified object, or INVALID_RMI if not found. */ - internal fun getId(implObject: T): Int { + internal fun getId(implObject: T): Int { return implObjects.getId(implObject) } + + + /** + * @return all the saved RMI implementation objects along with their RMI ID. This is used by session management in order to preserve RMI functionality. + */ + internal fun getAllImplObjects(): List> { + return implObjects.getAll() + } + + /** + * all the saved RMI implementation objects along with their RMI ID. This is used by session management in order to preserve RMI functionality. + */ + internal fun restoreImplObjects(implObjects: List>) { + this.implObjects.restoreAll(implObjects) + } + + internal open fun clear() { + this.implObjects.clear() + } } diff --git a/src/dorkbox/network/rmi/RmiSupportConnection.kt b/src/dorkbox/network/rmi/RmiSupportConnection.kt index 62d01446..03464551 100644 --- a/src/dorkbox/network/rmi/RmiSupportConnection.kt +++ b/src/dorkbox/network/rmi/RmiSupportConnection.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,15 @@ package dorkbox.network.rmi +import dorkbox.classUtil.ClassHelper import dorkbox.collections.LockFreeIntMap import dorkbox.network.connection.Connection -import dorkbox.network.connection.ListenerManager +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace import dorkbox.network.rmi.messages.ConnectionObjectCreateRequest import dorkbox.network.rmi.messages.ConnectionObjectDeleteRequest import dorkbox.network.serialization.Serialization -import dorkbox.util.classes.ClassHelper -import mu.KLogger +import org.slf4j.Logger +import java.lang.reflect.Proxy /** * Only the server can create or delete a global object @@ -34,20 +35,32 @@ import mu.KLogger * * Connection scope objects can be remotely created or deleted by either end of the connection. Only the server can create/delete a global scope object */ -class RmiSupportConnection internal constructor( - private val logger: KLogger, - private val connection: CONNECTION, - private val responseManager: ResponseManager, - private val serialization: Serialization, +class RmiSupportConnection : RmiObjectCache { + private val connection: CONNECTION + private val responseManager: ResponseManager + val serialization: Serialization private val getGlobalAction: (connection: CONNECTION, objectId: Int, interfaceClass: Class<*>) -> Any -) : RmiObjectCache(logger) { + internal constructor( + logger: Logger, + connection: CONNECTION, + responseManager: ResponseManager, + serialization: Serialization, + getGlobalAction: (connection: CONNECTION, objectId: Int, interfaceClass: Class<*>) -> Any + ) : super(logger) { + this.connection = connection + this.responseManager = responseManager + this.serialization = serialization + this.getGlobalAction = getGlobalAction + this.proxyObjects = LockFreeIntMap>() + this.remoteObjectCreationCallbacks = RemoteObjectStorage(logger) + } // It is critical that all of the RMI proxy objects are unique, and are saved/cached PER CONNECTION. These cannot be shared between connections! - private val proxyObjects = LockFreeIntMap() + private val proxyObjects: LockFreeIntMap> // callbacks for when a REMOTE object has been created - private val remoteObjectCreationCallbacks = RemoteObjectStorage(logger) + private val remoteObjectCreationCallbacks: RemoteObjectStorage /** * Removes a proxy object from the system @@ -58,29 +71,70 @@ class RmiSupportConnection internal constructor( return proxyObjects.remove(rmiId) != null } - private fun getProxyObject(rmiId: Int): RemoteObject? { + private fun getProxyObject(rmiId: Int): RemoteObject<*>? { return proxyObjects[rmiId] } - private fun saveProxyObject(rmiId: Int, remoteObject: RemoteObject) { + private fun saveProxyObject(rmiId: Int, remoteObject: RemoteObject<*>) { proxyObjects.put(rmiId, remoteObject) } - private fun registerCallback(callback: suspend Iface.() -> Unit): Int { + private fun registerCallback(callback: Iface.(Int) -> Unit): Int { return remoteObjectCreationCallbacks.register(callback) } - internal fun removeCallback(callbackId: Int): suspend Any.() -> Unit { + internal fun removeCallback(callbackId: Int): Any.(Int) -> Unit { // callback's area always correct, because we track them ourselves. return remoteObjectCreationCallbacks.remove(callbackId)!! } + internal fun getAllCallbacks(): List Unit>> { + @Suppress("UNCHECKED_CAST") + return remoteObjectCreationCallbacks.getAll() as List Unit>> + } + + internal fun restoreCallbacks(oldProxyCallbacks: List Unit>>) { + remoteObjectCreationCallbacks.restoreAll(oldProxyCallbacks) + } + + /** + * @return all the RMI proxy objects used by this connection. This is used by session management in order to preserve RMI functionality. + */ + internal fun getAllProxyObjects(): List> { + return proxyObjects.values.toList() + } + + /** + * Recreate all the proxy objects for this connection. This is used by session management in order to preserve RMI functionality. + */ + internal fun recreateProxyObjects(oldProxyObjects: List>) { + oldProxyObjects.forEach { + // the interface we care about is ALWAYS the second one! + val iface = it.javaClass.interfaces[1] + + val kryoId = connection.endPoint.serialization.getKryoIdForRmiClient(iface) + val rmiClient = Proxy.getInvocationHandler(it) as RmiClient + val rmiId = rmiClient.rmiObjectId + + val proxyObject = RmiManagerGlobal.createProxyObject( + rmiClient.isGlobal, + connection, + serialization, + responseManager, + kryoId, rmiId, + iface + ) + + saveProxyObject(rmiId, proxyObject) + } + } + /** * Tells us to save an existing object in the CONNECTION scope, so a remote connection can get it via [Connection.rmi.get()] * - * Methods that return a value will throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout]. + * NOTE:: Methods can throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout]. * * If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side * will have the proxy object replaced with the registered (non-proxy) object. @@ -98,7 +152,7 @@ class RmiSupportConnection internal constructor( val rmiId = saveImplObject(`object`) if (rmiId == RemoteObjectStorage.INVALID_RMI) { val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated") - ListenerManager.cleanStackTrace(exception) + exception.cleanStackTrace() logger.error("RMI error connection ${connection.id}", exception) } @@ -108,7 +162,7 @@ class RmiSupportConnection internal constructor( /** * Tells us to save an existing object in the CONNECTION scope using the specified ID, so a remote connection can get it via [Connection.rmi.get()] * - * Methods that return a value will throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout]. + * NOTE:: Methods can throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout]. * * If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side * will have the proxy object replaced with the registered (non-proxy) object. @@ -125,7 +179,7 @@ class RmiSupportConnection internal constructor( val success = saveImplObject(`object`, objectId) if (!success) { val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated") - ListenerManager.cleanStackTrace(exception) + exception.cleanStackTrace() logger.error("RMI error connection ${connection.id}", exception) } return success @@ -134,7 +188,8 @@ class RmiSupportConnection internal constructor( /** * Creates create a new proxy object where the implementation exists in a remote connection. * - * The callback will be notified when the remote object has been created. + * We use a callback to notify us when the object is ready. We can't "create this on the fly" because we + * have to wait for the object to be created + ID to be assigned on the remote system BEFORE we can create the proxy instance here. * * Methods that return a value will throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout]. * @@ -146,36 +201,47 @@ class RmiSupportConnection internal constructor( * * @see RemoteObject */ - fun create(vararg objectParameters: Any?, callback: suspend Iface.() -> Unit) { - val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(Function1::class.java, callback.javaClass, 0) + fun create(vararg objectParameters: Any?, callback: Iface.(rmiId: Int) -> Unit) { + val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(Function1::class.java, callback.javaClass, 0) ?: callback.javaClass val kryoId = serialization.getKryoIdForRmiClient(iFaceClass) @Suppress("UNCHECKED_CAST") objectParameters as Array - createRemoteObject(connection, kryoId, objectParameters, callback) + val callbackId = registerCallback(callback) + + // There is no rmiID yet, because we haven't created it! + val message = ConnectionObjectCreateRequest(RmiUtils.packShorts(callbackId, kryoId), objectParameters) + + connection.send(message) } /** * Creates create a new proxy object where the implementation exists in a remote connection. * - * The callback will be notified when the remote object has been created. + * We use a callback to notify us when the object is ready. We can't "create this on the fly" because we + * have to wait for the object to be created + ID to be assigned on the remote system BEFORE we can create the proxy instance here. * - * Methods that return a value will throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout]. + * NOTE:: Methods can throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout]. * * If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side * will have the proxy object replaced with the registered (non-proxy) object. * * If one wishes to change the default behavior, cast the object to access the different methods. - * ie: `val remoteObject = test as RemoteObject` + * ie: `val remoteObject = RemoteObject.cast(obj)` * * @see RemoteObject */ - fun create(callback: suspend Iface.() -> Unit) { - val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(Function1::class.java, callback.javaClass, 0) + fun create(callback: Iface.(rmiId: Int) -> Unit) { + val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(Function1::class.java, callback.javaClass, 0) ?: callback.javaClass val kryoId = serialization.getKryoIdForRmiClient(iFaceClass) - createRemoteObject(connection, kryoId, null, callback) + val callbackId = registerCallback(callback) + + // There is no rmiID yet, because we haven't created it! + val message = ConnectionObjectCreateRequest(RmiUtils.packShorts(callbackId, kryoId), null) + + connection.send(message) } /** @@ -188,12 +254,18 @@ class RmiSupportConnection internal constructor( fun delete(rmiObjectId: Int) { // we only create the proxy + execute the callback if the RMI id is valid! if (rmiObjectId == RemoteObjectStorage.INVALID_RMI) { - val exception = Exception("RMI ID '${rmiObjectId}' is invalid. Unable to delete RMI object!") - ListenerManager.cleanStackTrace(exception) + val exception = Exception("Unable to delete RMI object!") + exception.cleanStackTrace() logger.error("RMI error connection ${connection.id}", exception) return } + + // it DOESN'T matter which "side" we are, just delete both (RMI id's must always represent the same object on both sides) + removeProxyObject(rmiObjectId) + removeImplObject(rmiObjectId) + + // ALWAYS send a message because we don't know if we are the "client" or the "server" - and we want ALL sides cleaned up connection.send(ConnectionObjectDeleteRequest(rmiObjectId)) } @@ -204,6 +276,8 @@ class RmiSupportConnection internal constructor( * If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side * will have the proxy object replaced with the registered (non-proxy) object. * + *NOTE:: Methods can throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout]. + * * If one wishes to change the remote object behavior, cast the object to a [RemoteObject] to access the different methods, for example: * ie: `val remoteObject = test as RemoteObject` * @@ -220,6 +294,8 @@ class RmiSupportConnection internal constructor( /** * Gets a GLOBAL scope object via the ID. Global remote objects share their state among all connections. * + * NOTE:: Methods can throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout]. + * * If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side * will have the proxy object replaced with the registered (non-proxy) object. * @@ -237,13 +313,21 @@ class RmiSupportConnection internal constructor( } + /** + * Casts this remote object (specified by it's RMI ID) to the "RemoteObject" type, so that those methods can more easily be called + */ + inline fun cast(rmiId: Int): RemoteObject { + val obj = get(rmiId) + @Suppress("UNCHECKED_CAST") + return obj as RemoteObject + } /** * on the connection+client to get a connection-specific remote object (that exists on the server/client) */ internal fun getProxyObject(isGlobal: Boolean, connection: CONNECTION, rmiId: Int, interfaceClass: Class): Iface { - require(interfaceClass.isInterface) { "iface must be an interface." } + require(interfaceClass.isInterface) { "'interfaceClass' must be an interface!" } // so we can just instantly create the proxy object (or get the cached one) var proxyObject = getProxyObject(rmiId) @@ -269,10 +353,11 @@ class RmiSupportConnection internal constructor( * on the connection+client to get a connection-specific remote object (that exists on the server/client) */ internal fun getProxyObject(isGlobal: Boolean, connection: CONNECTION, kryoId: Int, rmiId: Int, interfaceClass: Class): Iface { - require(interfaceClass.isInterface) { "iface must be an interface." } + require(interfaceClass.isInterface) { "'interfaceClass' must be an interface!" } // so we can just instantly create the proxy object (or get the cached one) - var proxyObject = getProxyObject(rmiId) + @Suppress("UNCHECKED_CAST") + var proxyObject = getProxyObject(rmiId) as RemoteObject? if (proxyObject == null) { proxyObject = RmiManagerGlobal.createProxyObject(isGlobal, connection, @@ -289,25 +374,9 @@ class RmiSupportConnection internal constructor( return proxyObject as Iface } - - /** - * on the "client" to create a connection-specific remote object (that exists on the server) - */ - private fun createRemoteObject(connection: CONNECTION, kryoId: Int, objectParameters: Array?, callback: suspend Iface.() -> Unit) { - val callbackId = registerCallback(callback) - - // There is no rmiID yet, because we haven't created it! - val message = ConnectionObjectCreateRequest(RmiUtils.packShorts(callbackId, kryoId), objectParameters) - - // We use a callback to notify us when the object is ready. We can't "create this on the fly" because we - // have to wait for the object to be created + ID to be assigned on the remote system BEFORE we can create the proxy instance here. - - // this means we are creating a NEW object on the server - connection.send(message) - } - - internal fun clear() { + override fun clear() { + super.clear() proxyObjects.clear() - remoteObjectCreationCallbacks.close() + remoteObjectCreationCallbacks.clear() } } diff --git a/src/dorkbox/network/rmi/RmiSupportServer.kt b/src/dorkbox/network/rmi/RmiSupportServer.kt index e49cb019..8911bdbb 100644 --- a/src/dorkbox/network/rmi/RmiSupportServer.kt +++ b/src/dorkbox/network/rmi/RmiSupportServer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ package dorkbox.network.rmi import dorkbox.network.connection.Connection -import dorkbox.network.connection.ListenerManager -import mu.KLogger +import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace +import org.slf4j.Logger /** * Only the server can create or delete a global object @@ -30,7 +30,7 @@ import mu.KLogger * Connection scope objects can be remotely created or deleted by either end of the connection. Only the server can create/delete a global scope object */ class RmiSupportServer internal constructor( - private val logger: KLogger, + private val logger: Logger, private val rmiGlobalSupport: RmiManagerGlobal ) { /** @@ -53,7 +53,7 @@ class RmiSupportServer internal constructor( val rmiId = rmiGlobalSupport.saveImplObject(`object`) if (rmiId == RemoteObjectStorage.INVALID_RMI) { val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated") - ListenerManager.cleanStackTrace(exception) + exception.cleanStackTrace() logger.error("RMI error", exception) } return rmiId @@ -79,7 +79,7 @@ class RmiSupportServer internal constructor( val success = rmiGlobalSupport.saveImplObject(`object`, objectId) if (!success) { val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated") - ListenerManager.cleanStackTrace(exception) + exception.cleanStackTrace() logger.error("RMI error", exception) } return success @@ -103,7 +103,7 @@ class RmiSupportServer internal constructor( rmiGlobalSupport.removeImplObject(successRmiId) } else { val exception = Exception("RMI implementation '${`object`::class.java}' could not be deleted! It does not exist") - ListenerManager.cleanStackTrace(exception) + exception.cleanStackTrace() logger.error("RMI error", exception) } @@ -126,9 +126,10 @@ class RmiSupportServer internal constructor( val success = previousObject != null if (!success) { val exception = Exception("RMI implementation UD '$objectId' could not be deleted! It does not exist") - ListenerManager.cleanStackTrace(exception) + exception.cleanStackTrace() logger.error("RMI error", exception) } + return success } } diff --git a/src/dorkbox/network/rmi/RmiUtils.kt b/src/dorkbox/network/rmi/RmiUtils.kt index 8c491a3c..08ababf5 100644 --- a/src/dorkbox/network/rmi/RmiUtils.kt +++ b/src/dorkbox/network/rmi/RmiUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,9 @@ package dorkbox.network.rmi import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.reflectasm.MethodAccess +import dorkbox.classUtil.ClassHelper import dorkbox.network.connection.Connection -import dorkbox.util.classes.ClassHelper -import mu.KLogger +import org.slf4j.Logger import java.lang.reflect.Method import java.lang.reflect.Modifier import java.util.* @@ -75,7 +75,7 @@ object RmiUtils { throw RuntimeException("Two methods with same signature! ('$o1Name', '$o2Name'") } - private fun getReflectAsmMethod(logger: KLogger, clazz: Class<*>): MethodAccess? { + private fun getReflectAsmMethod(logger: Logger, clazz: Class<*>): MethodAccess? { return try { val methodAccess = MethodAccess.get(clazz) @@ -95,7 +95,7 @@ object RmiUtils { * @param iFace this is never null. * @param impl this is NULL on the rmi "client" side. This is NOT NULL on the "server" side (where the object lives) */ - fun getCachedMethods(logger: KLogger, kryo: Kryo, asmEnabled: Boolean, iFace: Class<*>, impl: Class<*>?, classId: Int): Array { + fun getCachedMethods(logger: Logger, kryo: Kryo, asmEnabled: Boolean, iFace: Class<*>, impl: Class<*>?, classId: Int): Array { var ifaceAsmMethodAccess: MethodAccess? = null var implAsmMethodAccess: MethodAccess? = null @@ -495,7 +495,7 @@ object RmiUtils { * * We do this because these stack frames are not useful in resolving exception handling from a users perspective, and only clutter the stacktrace. */ - fun cleanStackTraceForProxy(localException: Exception, remoteException: Exception? = null) { + fun cleanStackTraceForProxy(localException: Throwable, remoteException: Throwable? = null) { val myClassName = RmiClient::class.java.name val stackTrace = localException.stackTrace var newStartIndex = 0 @@ -553,7 +553,7 @@ object RmiUtils { * * Neither of these are useful in resolving exception handling from a users perspective, and only clutter the stacktrace. */ - fun cleanStackTraceForImpl(exception: Exception, isSuspendFunction: Boolean) { + fun cleanStackTraceForImpl(exception: Throwable, isSuspendFunction: Boolean) { val packageName = RmiUtils::class.java.packageName val stackTrace = exception.stackTrace @@ -578,7 +578,8 @@ object RmiUtils { // step 2: starting at newEndIndex -> 0, find the start of reflection information (we are java11+ ONLY, so this is easy) for (i in newEndIndex downTo 0) { // this will be either JAVA reflection or ReflectASM reflection - val stackModule = stackTrace[i].moduleName + val stackTraceElement: StackTraceElement = stackTrace[i] + val stackModule = stackTraceElement.moduleName if (stackModule == "java.base") { newEndIndex-- } else { diff --git a/src/dorkbox/network/rmi/messages/ContinuationSerializer.kt b/src/dorkbox/network/rmi/messages/ContinuationSerializer.kt index 8a911bb8..2f7e64ed 100644 --- a/src/dorkbox/network/rmi/messages/ContinuationSerializer.kt +++ b/src/dorkbox/network/rmi/messages/ContinuationSerializer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import kotlin.coroutines.Continuation -class ContinuationSerializer() : Serializer>() { +internal class ContinuationSerializer() : Serializer>() { init { isImmutable = true } diff --git a/src/dorkbox/network/rmi/messages/MethodRequest.kt b/src/dorkbox/network/rmi/messages/MethodRequest.kt index 58e85ae5..98ee5894 100644 --- a/src/dorkbox/network/rmi/messages/MethodRequest.kt +++ b/src/dorkbox/network/rmi/messages/MethodRequest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package dorkbox.network.rmi.messages import dorkbox.network.rmi.CachedMethod import dorkbox.network.rmi.RmiUtils +import java.util.* /** * Internal message to invoke methods remotely. @@ -47,6 +48,21 @@ class MethodRequest : RmiMessage { var args: Array? = null override fun toString(): String { - return "MethodRequest(isGlobal=$isGlobal, rmiObjectId=${RmiUtils.unpackLeft(packedId)}, rmiId=${RmiUtils.unpackRight(packedId)}, cachedMethod=$cachedMethod, args=${args?.contentToString()})" + var argString = "" + val args1 = args + if (!args1.isNullOrEmpty()) { + // long byte arrays have SERIOUS problems! + argString = Arrays.deepToString(args1.map { + when (it) { + is ByteArray -> { "${it::class.java.simpleName}(length=${it.size})"} + is Array<*> -> { "${it::class.java.simpleName}(length=${it.size})"} + is Collection<*> -> { "${it::class.java.simpleName}(length=${it.size})"} + else -> { it } + } + }.toTypedArray()) + argString = argString.substring(1, argString.length - 1) + } + + return "MethodRequest(isGlobal=$isGlobal, rmiObjectId=${RmiUtils.unpackLeft(packedId)}, rmiId=${RmiUtils.unpackRight(packedId)}, cachedMethod=$cachedMethod, args=${argString})" } } diff --git a/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt b/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt index f3f34c49..687bf2ec 100644 --- a/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt +++ b/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,7 +12,8 @@ * 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. - * + */ +/* * Copyright (c) 2008, Nathan Sweet * All rights reserved. * @@ -42,7 +43,7 @@ import com.esotericsoftware.kryo.io.Output import dorkbox.network.connection.Connection import dorkbox.network.rmi.CachedMethod import dorkbox.network.rmi.RmiUtils -import dorkbox.network.serialization.KryoExtra +import dorkbox.network.serialization.KryoReader import org.agrona.collections.Int2ObjectHashMap import java.lang.reflect.Method @@ -50,7 +51,7 @@ import java.lang.reflect.Method * Internal message to invoke methods remotely. */ @Suppress("ConstantConditionIf") -class MethodRequestSerializer(private val methodCache: Int2ObjectHashMap>) : Serializer() { +internal class MethodRequestSerializer(private val methodCache: Int2ObjectHashMap>) : Serializer() { override fun write(kryo: Kryo, output: Output, methodRequest: MethodRequest) { val method = methodRequest.cachedMethod @@ -83,7 +84,7 @@ class MethodRequestSerializer(private val methodCache: I val methodIndex = RmiUtils.unpackRight(methodInfo) val isGlobal = input.readBoolean() - kryo as KryoExtra + kryo as KryoReader val cachedMethod = try { methodCache[methodClassId][methodIndex] diff --git a/src/dorkbox/network/rmi/messages/MethodResponseSerializer.kt b/src/dorkbox/network/rmi/messages/MethodResponseSerializer.kt index 3ae02cab..cb48e7e6 100644 --- a/src/dorkbox/network/rmi/messages/MethodResponseSerializer.kt +++ b/src/dorkbox/network/rmi/messages/MethodResponseSerializer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output -class MethodResponseSerializer() : Serializer() { +internal class MethodResponseSerializer() : Serializer() { override fun write(kryo: Kryo, output: Output, response: MethodResponse) { output.writeInt(response.packedId) kryo.writeClassAndObject(output, response.result) diff --git a/src/dorkbox/network/rmi/messages/RmiClientSerializer.kt b/src/dorkbox/network/rmi/messages/RmiClientSerializer.kt index 28f06dcd..e5b02336 100644 --- a/src/dorkbox/network/rmi/messages/RmiClientSerializer.kt +++ b/src/dorkbox/network/rmi/messages/RmiClientSerializer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import dorkbox.network.connection.Connection import dorkbox.network.connection.EndPoint import dorkbox.network.rmi.RmiClient import dorkbox.network.rmi.RmiSupportConnection -import dorkbox.network.serialization.KryoExtra +import dorkbox.network.serialization.KryoReader import java.lang.reflect.Proxy /** @@ -56,7 +56,7 @@ import java.lang.reflect.Proxy * If the impl object 'lives' on the SERVER, then the server must tell the client about the iface ID */ @Suppress("UNCHECKED_CAST") -class RmiClientSerializer: Serializer() { +internal class RmiClientSerializer: Serializer() { override fun write(kryo: Kryo, output: Output, proxyObject: Any) { val handler = Proxy.getInvocationHandler(proxyObject) as RmiClient output.writeBoolean(handler.isGlobal) @@ -67,7 +67,7 @@ class RmiClientSerializer: Serializer() { val isGlobal = input.readBoolean() val objectId = input.readInt(true) - kryo as KryoExtra + kryo as KryoReader val endPoint: EndPoint = kryo.connection.endPoint as EndPoint return if (isGlobal) { diff --git a/src/dorkbox/network/rmi/messages/RmiServerSerializer.kt b/src/dorkbox/network/rmi/messages/RmiServerSerializer.kt index d6e00724..e3b69d4f 100644 --- a/src/dorkbox/network/rmi/messages/RmiServerSerializer.kt +++ b/src/dorkbox/network/rmi/messages/RmiServerSerializer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,7 +12,8 @@ * 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. - * + */ +/* * Copyright (c) 2008, Nathan Sweet * All rights reserved. * @@ -41,7 +42,8 @@ import com.esotericsoftware.kryo.io.Output import dorkbox.network.connection.Connection import dorkbox.network.rmi.RemoteObjectStorage import dorkbox.network.rmi.RmiSupportConnection -import dorkbox.network.serialization.KryoExtra +import dorkbox.network.serialization.KryoReader +import dorkbox.network.serialization.KryoWriter /** * This is to manage serializing RMI objects across the wire... @@ -73,10 +75,10 @@ import dorkbox.network.serialization.KryoExtra * If the impl object 'lives' on the SERVER, then the server must tell the client about the iface ID */ @Suppress("UNCHECKED_CAST") -class RmiServerSerializer : Serializer(false) { +internal class RmiServerSerializer : Serializer(false) { override fun write(kryo: Kryo, output: Output, `object`: Any) { - val kryoExtra = kryo as KryoExtra + val kryoExtra = kryo as KryoWriter val connection = kryoExtra.connection val rmi = connection.rmi // have to write what the rmi ID is ONLY. A remote object sent via a connection IS ONLY a connection-scope object! @@ -96,7 +98,7 @@ class RmiServerSerializer : Serializer(false) { } override fun read(kryo: Kryo, input: Input, interfaceClass: Class<*>): Any? { - val kryoExtra = kryo as KryoExtra + val kryoExtra = kryo as KryoReader val rmiId = input.readInt(true) val connection = kryoExtra.connection diff --git a/src/dorkbox/network/rmi/messages/package-info.java b/src/dorkbox/network/rmi/messages/package-info.java new file mode 100644 index 00000000..0bcbd00b --- /dev/null +++ b/src/dorkbox/network/rmi/messages/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.rmi.messages; diff --git a/src/dorkbox/network/rmi/package-info.java b/src/dorkbox/network/rmi/package-info.java new file mode 100644 index 00000000..8d61ab37 --- /dev/null +++ b/src/dorkbox/network/rmi/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.rmi; diff --git a/src/dorkbox/network/serialization/AeronInput.kt b/src/dorkbox/network/serialization/AeronInput.kt index 3d6ffca7..cbf093a8 100644 --- a/src/dorkbox/network/serialization/AeronInput.kt +++ b/src/dorkbox/network/serialization/AeronInput.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,7 +12,8 @@ * 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. - * + */ +/* * Copyright (c) 2008, Nathan Sweet * All rights reserved. * @@ -56,7 +57,7 @@ import java.io.InputStream * * Modified from KRYO ByteBufferInput to use ByteBuf instead of ByteBuffer. */ -class AeronInput +open class AeronInput /** Creates an uninitialized Input, [.setBuffer] must be called before the Input is used. */ () : Input() { diff --git a/src/dorkbox/network/serialization/AeronOutput.kt b/src/dorkbox/network/serialization/AeronOutput.kt index a4cee67c..94dcfaa5 100644 --- a/src/dorkbox/network/serialization/AeronOutput.kt +++ b/src/dorkbox/network/serialization/AeronOutput.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,30 +12,12 @@ * 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. - * - * Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package dorkbox.network.serialization import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.io.Output +import com.esotericsoftware.kryo.util.Util import org.agrona.ExpandableDirectByteBuffer import org.agrona.MutableDirectBuffer import org.agrona.concurrent.UnsafeBuffer @@ -55,7 +37,7 @@ import java.io.OutputStream * * Modified from KRYO to use ByteBuf. */ -class AeronOutput : Output { +open class AeronOutput : Output { /** Returns the buffer. The bytes between zero and [.position] are the data that has been written. */ // NOTE: capacity IS NOT USED! @@ -106,7 +88,7 @@ class AeronOutput : Output { */ @Deprecated("This buffer does not used a byte[]") override fun getBuffer(): ByteArray { - throw UnsupportedOperationException("This buffer does not used a byte[], see #getInternaleBuffer().") + throw UnsupportedOperationException("This buffer does not use a byte[], see #getInternaleBuffer().") } /** @@ -130,9 +112,9 @@ class AeronOutput : Output { /** * Sets a new buffer to write to. The max size is the buffer's length. */ - @Deprecated("maxBufferSize parameter is ignored", ReplaceWith("setBuffer(buffer)")) override fun setBuffer(buffer: ByteArray, maxBufferSize: Int) { setBuffer(buffer) + maxCapacity = if (maxBufferSize == -1) Util.maxArraySize else maxBufferSize } override fun toBytes(): ByteArray { diff --git a/src/dorkbox/network/serialization/ClassRegistration.kt b/src/dorkbox/network/serialization/ClassRegistration.kt index a9d4fabc..598d40ee 100644 --- a/src/dorkbox/network/serialization/ClassRegistration.kt +++ b/src/dorkbox/network/serialization/ClassRegistration.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import com.esotericsoftware.kryo.Serializer import dorkbox.network.connection.Connection import dorkbox.network.rmi.messages.RmiServerSerializer -internal abstract class ClassRegistration(val clazz: Class<*>, val serializer: Serializer<*>? = null, var id: Int = 0) { +internal abstract class ClassRegistration(val clazz: Class<*>, val serializer: Serializer<*>? = null, var id: Int = 0) { companion object { const val IGNORE_REGISTRATION = -1 } @@ -33,7 +33,7 @@ internal abstract class ClassRegistration(val clazz: Cla * If so, we ignore it - any IFACE or IMPL that already has been assigned to an RMI serializer, *MUST* remain an RMI serializer * If this class registration will EVENTUALLY be for RMI, then [ClassRegistrationForRmi] will reassign the serializer */ - open fun register(kryo: KryoExtra, rmi: RmiHolder) { + open fun register(kryo: Kryo, rmi: RmiHolder) { // ClassRegistrationForRmi overrides this method if (id == IGNORE_REGISTRATION) { // we have previously specified that this registration should be ignored! diff --git a/src/dorkbox/network/serialization/ClassRegistration0.kt b/src/dorkbox/network/serialization/ClassRegistration0.kt index 6224d850..035610c6 100644 --- a/src/dorkbox/network/serialization/ClassRegistration0.kt +++ b/src/dorkbox/network/serialization/ClassRegistration0.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.Serializer import dorkbox.network.connection.Connection -internal class ClassRegistration0(clazz: Class<*>, serializer: Serializer<*>) : ClassRegistration(clazz, serializer) { +internal class ClassRegistration0(clazz: Class<*>, serializer: Serializer<*>) : ClassRegistration(clazz, serializer) { override fun register(kryo: Kryo) { id = kryo.register(clazz, serializer).id info = "Registered $id -> ${clazz.name} using ${serializer!!.javaClass.name}" diff --git a/src/dorkbox/network/serialization/ClassRegistration1.kt b/src/dorkbox/network/serialization/ClassRegistration1.kt index 8b9113a7..1c23bab3 100644 --- a/src/dorkbox/network/serialization/ClassRegistration1.kt +++ b/src/dorkbox/network/serialization/ClassRegistration1.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ package dorkbox.network.serialization import com.esotericsoftware.kryo.Kryo import dorkbox.network.connection.Connection -internal class ClassRegistration1(clazz: Class<*>, id: Int) : ClassRegistration(clazz, null, id) { +internal class ClassRegistration1(clazz: Class<*>, id: Int) : ClassRegistration(clazz, null, id) { override fun register(kryo: Kryo) { kryo.register(clazz, id) info = "Registered $id -> (specified) ${clazz.name}" diff --git a/src/dorkbox/network/serialization/ClassRegistration2.kt b/src/dorkbox/network/serialization/ClassRegistration2.kt index a7500613..3fd89713 100644 --- a/src/dorkbox/network/serialization/ClassRegistration2.kt +++ b/src/dorkbox/network/serialization/ClassRegistration2.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.Serializer import dorkbox.network.connection.Connection -internal class ClassRegistration2(clazz: Class<*>, serializer: Serializer<*>, id: Int) : ClassRegistration(clazz, serializer, id) { +internal class ClassRegistration2(clazz: Class<*>, serializer: Serializer<*>, id: Int) : ClassRegistration(clazz, serializer, id) { override fun register(kryo: Kryo) { kryo.register(clazz, serializer, id) diff --git a/src/dorkbox/network/serialization/ClassRegistration3.kt b/src/dorkbox/network/serialization/ClassRegistration3.kt index 02f650cf..300b0af4 100644 --- a/src/dorkbox/network/serialization/ClassRegistration3.kt +++ b/src/dorkbox/network/serialization/ClassRegistration3.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ package dorkbox.network.serialization import com.esotericsoftware.kryo.Kryo import dorkbox.network.connection.Connection -internal open class ClassRegistration3(clazz: Class<*>) : ClassRegistration(clazz) { +internal open class ClassRegistration3(clazz: Class<*>) : ClassRegistration(clazz) { override fun register(kryo: Kryo) { id = kryo.register(clazz).id diff --git a/src/dorkbox/network/serialization/ClassRegistrationForRmi.kt b/src/dorkbox/network/serialization/ClassRegistrationForRmi.kt index 1125ad1a..deb2e6f0 100644 --- a/src/dorkbox/network/serialization/ClassRegistrationForRmi.kt +++ b/src/dorkbox/network/serialization/ClassRegistrationForRmi.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package dorkbox.network.serialization +import com.esotericsoftware.kryo.Kryo import dorkbox.network.connection.Connection import dorkbox.network.rmi.messages.RmiServerSerializer @@ -47,7 +48,7 @@ import dorkbox.network.rmi.messages.RmiServerSerializer */ internal class ClassRegistrationForRmi(ifaceClass: Class<*>, var implClass: Class<*>?, - serializer: RmiServerSerializer) : ClassRegistration(ifaceClass, serializer) { + serializer: RmiServerSerializer) : ClassRegistration(ifaceClass, serializer) { /** * In general: * @@ -104,7 +105,7 @@ internal class ClassRegistrationForRmi(ifaceClass: Class * send: register IMPL object class with RmiServerSerializer * lookup IMPL object -> rmiID */ - override fun register(kryo: KryoExtra, rmi: RmiHolder) { + override fun register(kryo: Kryo, rmi: RmiHolder) { // we override this, because we ALWAYS will call our RMI registration! if (id == IGNORE_REGISTRATION) { // we have previously specified that this registration should be ignored! diff --git a/src/dorkbox/network/serialization/DisconnectSerializer.kt b/src/dorkbox/network/serialization/DisconnectSerializer.kt new file mode 100644 index 00000000..d8205dd0 --- /dev/null +++ b/src/dorkbox/network/serialization/DisconnectSerializer.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import dorkbox.network.connection.DisconnectMessage + +internal class DisconnectSerializer: Serializer() { + override fun write(kryo: Kryo, output: Output, `object`: DisconnectMessage) { + output.writeBoolean(`object`.closeEverything) + } + + override fun read(kryo: Kryo, input: Input, type: Class): DisconnectMessage { + val closeEverything = input.readBoolean() + return if (closeEverything) { + DisconnectMessage.CLOSE_EVERYTHING + } else { + DisconnectMessage.CLOSE_SIMPLE + } + } +} diff --git a/src/dorkbox/network/serialization/FileContentsSerializer.kt b/src/dorkbox/network/serialization/FileContentsSerializer.kt new file mode 100644 index 00000000..3aff0dc6 --- /dev/null +++ b/src/dorkbox/network/serialization/FileContentsSerializer.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import dorkbox.network.connection.Connection +import dorkbox.network.connection.CryptoManagement +import dorkbox.network.connection.EndPoint +import dorkbox.network.connection.streaming.StreamingManager +import java.io.File + +internal class FileContentsSerializer : Serializer() { + lateinit var streamingManager: StreamingManager + + init { + isImmutable = true + } + + @Suppress("UNCHECKED_CAST") + override fun write(kryo: Kryo, output: Output, file: File) { + val kryoExtra = kryo as KryoWriter + val connection = kryoExtra.connection + val publication = connection.publication + val endPoint = connection.endPoint as EndPoint + val sendIdleStrategy = connection.sendIdleStrategy + + // NOTE: the stream session ID is a combination of the connection ID + random ID (on the receiving side) + val streamSessionId = CryptoManagement.secureRandom.nextInt() + + // use the streaming manager to send the file in blocks to the remote endpoint + val kryo = endPoint.serialization.take() + try { + streamingManager.sendFile( + file = file, + publication = publication, + endPoint = endPoint, + kryo = kryo, + sendIdleStrategy = sendIdleStrategy, + connection = connection, + streamSessionId = streamSessionId + ) + } finally { + endPoint.serialization.put(kryo) + } + +// output.writeString(file.path) + output.writeInt(streamSessionId, true) + } + + @Suppress("UNCHECKED_CAST") + override fun read(kryo: Kryo, input: Input, type: Class): File { + val kryoExtra = kryo as KryoReader + val connection = kryoExtra.connection + val endPoint = connection.endPoint as EndPoint + + +// val path = input.readString() + val streamSessionId = input.readInt(true) + + // get the file object out of the streaming manager!!! + val file = streamingManager.getFile(connection, endPoint, streamSessionId) + + return file + } +} diff --git a/src/dorkbox/network/serialization/KryoExtra.kt b/src/dorkbox/network/serialization/KryoExtra.kt deleted file mode 100644 index ae216085..00000000 --- a/src/dorkbox/network/serialization/KryoExtra.kt +++ /dev/null @@ -1,563 +0,0 @@ -/* - * Copyright 2020 dorkbox, llc - * - * 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 dorkbox.network.serialization - -import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.io.Input -import com.esotericsoftware.kryo.io.Output -import dorkbox.network.connection.Connection -import org.agrona.DirectBuffer - -/** - * READ and WRITE are exclusive to each other and can be performed in different threads. - */ -class KryoExtra() : Kryo() { - // for kryo serialization - internal val readerBuffer = AeronInput() - internal val writerBuffer = AeronOutput() - - // crypto + compression have to work with native byte arrays, so here we go... -// private val reader = Input(ABSOLUTE_MAX_SIZE_OBJECT) -// private val writer = Output(ABSOLUTE_MAX_SIZE_OBJECT) -// private val temp = ByteArray(ABSOLUTE_MAX_SIZE_OBJECT) - - // This is unique per connection. volatile/etc is not necessary because it is set/read in the same thread - lateinit var connection: CONNECTION - -// private val secureRandom = SecureRandom() -// private var cipher: Cipher? = null -// private val compressor = factory.fastCompressor() -// private val decompressor = factory.fastDecompressor() - -// -// companion object { -// private const val ABSOLUTE_MAX_SIZE_OBJECT = 500000 // by default, this is about 500k -// private const val DEBUG = false -// -// // snappycomp : 7.534 micros/op; 518.5 MB/s (output: 55.1%) -// // snappyuncomp : 1.391 micros/op; 2808.1 MB/s -// // lz4comp : 6.210 micros/op; 629.0 MB/s (output: 55.4%) -// // lz4uncomp : 0.641 micros/op; 6097.9 MB/s -// private val factory = LZ4Factory.fastestInstance() -// private const val ALGORITHM = "AES/GCM/NoPadding" -// private const val TAG_LENGTH_BIT = 128 -// private const val IV_LENGTH_BYTE = 12 -// } - -// init { -// cipher = try { -// Cipher.getInstance(ALGORITHM) -// } catch (e: Exception) { -// throw IllegalStateException("could not get cipher instance", e) -// } -// } - - /** - * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! - * - * OUTPUT: - * ++++++++++++++++++++++++++ - * + class and object bytes + - * ++++++++++++++++++++++++++ - */ - @Throws(Exception::class) - fun write(message: Any): AeronOutput { - writerBuffer.reset() - writeClassAndObject(writerBuffer, message) - return writerBuffer - } - - /** - * OUTPUT: - * ++++++++++++++++++++++++++ - * + class and object bytes + - * ++++++++++++++++++++++++++ - */ - @Throws(Exception::class) - fun write(connection: CONNECTION, message: Any): AeronOutput { - // required by RMI and some serializers to determine which connection wrote (or has info about) this object - this.connection = connection - - writerBuffer.reset() - writeClassAndObject(writerBuffer, message) - return writerBuffer - } - - /** - * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! - * - * INPUT: - * ++++++++++++++++++++++++++ - * + class and object bytes + - * ++++++++++++++++++++++++++ - */ - @Throws(Exception::class) - fun read(buffer: DirectBuffer): Any { - // this properly sets the buffer info - readerBuffer.setBuffer(buffer, 0, buffer.capacity()) - return readClassAndObject(readerBuffer) - } - - /** - * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! - * - * INPUT: - * ++++++++++++++++++++++++++ - * + class and object bytes + - * ++++++++++++++++++++++++++ - */ - @Throws(Exception::class) - fun read(buffer: DirectBuffer, offset: Int, length: Int): Any { - // this properly sets the buffer info - readerBuffer.setBuffer(buffer, offset, length) - return readClassAndObject(readerBuffer) - } - - /** - * INPUT: - * ++++++++++++++++++++++++++ - * + class and object bytes + - * ++++++++++++++++++++++++++ - */ - @Throws(Exception::class) - fun read(buffer: DirectBuffer, offset: Int, length: Int, connection: CONNECTION): Any? { - // required by RMI and some serializers to determine which connection wrote (or has info about) this object - this.connection = connection - - // this properly sets the buffer info - readerBuffer.setBuffer(buffer, offset, length) - return readClassAndObject(readerBuffer) - } - - - - - //////////////// - //////////////// - //////////////// - // for more complicated writes, sadly, we have to deal DIRECTLY with byte arrays - //////////////// - //////////////// - //////////////// - - /** - * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! - * - * OUTPUT: - * ++++++++++++++++++++++++++ - * + class and object bytes + - * ++++++++++++++++++++++++++ - */ - fun write(writer: Output, message: Any) { - // write the object to the NORMAL output buffer! - writer.reset() - writeClassAndObject(writer, message) - } - - /** - * OUTPUT: - * ++++++++++++++++++++++++++ - * + class and object bytes + - * ++++++++++++++++++++++++++ - */ - private fun write(connection: CONNECTION, writer: Output, message: Any) { - // required by RMI and some serializers to determine which connection wrote (or has info about) this object - this.connection = connection - - // write the object to the NORMAL output buffer! - writer.reset() - writeClassAndObject(writer, message) - } - - /** - * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! - * - * INPUT: - * ++++++++++++++++++++++++++ - * + class and object bytes + - * ++++++++++++++++++++++++++ - */ - fun read(reader: Input): Any { - return readClassAndObject(reader) - } - - /** - * INPUT: - * ++++++++++++++++++++++++++ - * + class and object bytes + - * ++++++++++++++++++++++++++ - */ - private fun read(connection: CONNECTION, reader: Input): Any { - // required by RMI and some serializers to determine which connection wrote (or has info about) this object - this.connection = connection - - return readClassAndObject(reader) - } -// -// /** -// * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! -// * -// * BUFFER: -// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * + uncompressed length (1-4 bytes) + compressed data + -// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * -// * COMPRESSED DATA: -// * ++++++++++++++++++++++++++ -// * + class and object bytes + -// * ++++++++++++++++++++++++++ -// */ -// fun writeCompressed(logger: Logger, output: Output, message: Any) { -// // write the object to a TEMP buffer! this will be compressed later -// write(writer, message) -// -// // save off how much data the object took -// val length = writer.position() -// val maxCompressedLength = compressor.maxCompressedLength(length) -// -// ////////// compressing data -// // we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger -// // output), will be negated by the increase in size by the encryption -// val compressOutput = temp -// -// // LZ4 compress. -// val compressedLength = compressor.compress(writer.buffer, 0, length, compressOutput, 0, maxCompressedLength) -// -// if (DEBUG) { -// val orig = Sys.bytesToHex(writer.buffer, 0, length) use String.toHexBytes() instead -// val compressed = Sys.bytesToHex(compressOutput, 0, compressedLength) -// logger.error(OS.LINE_SEPARATOR + -// "ORIG: (" + length + ")" + OS.LINE_SEPARATOR + orig + -// OS.LINE_SEPARATOR + -// "COMPRESSED: (" + compressedLength + ")" + OS.LINE_SEPARATOR + compressed) -// } -// -// // now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version -// output.writeInt(length, true) -// -// // have to copy over the orig data, because we used the temp buffer. Also have to account for the length of the uncompressed size -// output.writeBytes(compressOutput, 0, compressedLength) -// } -// -// /** -// * BUFFER: -// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * + uncompressed length (1-4 bytes) + compressed data + -// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * -// * COMPRESSED DATA: -// * ++++++++++++++++++++++++++ -// * + class and object bytes + -// * ++++++++++++++++++++++++++ -// */ -// fun writeCompressed(logger: Logger, connection: Connection, output: Output, message: Any) { -// // write the object to a TEMP buffer! this will be compressed later -// write(connection, writer, message) -// -// // save off how much data the object took -// val length = writer.position() -// val maxCompressedLength = compressor.maxCompressedLength(length) -// -// ////////// compressing data -// // we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger -// // output), will be negated by the increase in size by the encryption -// val compressOutput = temp -// -// // LZ4 compress. -// val compressedLength = compressor.compress(writer.buffer, 0, length, compressOutput, 0, maxCompressedLength) -// -// if (DEBUG) { -// val orig = Sys.bytesToHex(writer.buffer, 0, length) -// val compressed = Sys.bytesToHex(compressOutput, 0, compressedLength) -// logger.error(OS.LINE_SEPARATOR + -// "ORIG: (" + length + ")" + OS.LINE_SEPARATOR + orig + -// OS.LINE_SEPARATOR + -// "COMPRESSED: (" + compressedLength + ")" + OS.LINE_SEPARATOR + compressed) -// } -// -// // now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version -// output.writeInt(length, true) -// -// // have to copy over the orig data, because we used the temp buffer. Also have to account for the length of the uncompressed size -// output.writeBytes(compressOutput, 0, compressedLength) -// } -// -// /** -// * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! -// * -// * BUFFER: -// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * + uncompressed length (1-4 bytes) + compressed data + -// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * -// * COMPRESSED DATA: -// * ++++++++++++++++++++++++++ -// * + class and object bytes + -// * ++++++++++++++++++++++++++ -// */ -// fun readCompressed(logger: Logger, input: Input, length: Int): Any { -// //////////////// -// // Note: we CANNOT write BACK to the buffer as "temp" storage, since there could be additional data on it! -// //////////////// -// -// // get the decompressed length (at the beginning of the array) -// var length = length -// val uncompressedLength = input.readInt(true) -// if (uncompressedLength > ABSOLUTE_MAX_SIZE_OBJECT) { -// throw IOException("Uncompressed size ($uncompressedLength) is larger than max allowed size ($ABSOLUTE_MAX_SIZE_OBJECT)!") -// } -// -// // because 1-4 bytes for the decompressed size (this number is never negative) -// val lengthLength = OptimizeUtilsByteArray.intLength(uncompressedLength, true) -// val start = input.position() -// -// // have to adjust for uncompressed length-length -// length = length - lengthLength -// -// -// ///////// decompress data -// input.readBytes(temp, 0, length) -// -// -// // LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor) -// reader.reset() -// decompressor.decompress(temp, 0, reader.buffer, 0, uncompressedLength) -// reader.setLimit(uncompressedLength) -// -// if (DEBUG) { -// val compressed = Sys.bytesToHex(temp, start, length) -// val orig = Sys.bytesToHex(reader.buffer, start, uncompressedLength) -// logger.error(OS.LINE_SEPARATOR + -// "COMPRESSED: (" + length + ")" + OS.LINE_SEPARATOR + compressed + -// OS.LINE_SEPARATOR + -// "ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig) -// } -// -// // read the object from the buffer. -// return read(reader) -// } -// -// /** -// * BUFFER: -// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * + uncompressed length (1-4 bytes) + compressed data + -// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * -// * COMPRESSED DATA: -// * ++++++++++++++++++++++++++ -// * + class and object bytes + -// * ++++++++++++++++++++++++++ -// */ -// fun readCompressed(logger: Logger, connection: Connection, input: Input, length: Int): Any { -// //////////////// -// // Note: we CANNOT write BACK to the buffer as "temp" storage, since there could be additional data on it! -// //////////////// -// -// // get the decompressed length (at the beginning of the array) -// var length = length -// val uncompressedLength = input.readInt(true) -// if (uncompressedLength > ABSOLUTE_MAX_SIZE_OBJECT) { -// throw IOException("Uncompressed size ($uncompressedLength) is larger than max allowed size ($ABSOLUTE_MAX_SIZE_OBJECT)!") -// } -// -// // because 1-4 bytes for the decompressed size (this number is never negative) -// val lengthLength = OptimizeUtilsByteArray.intLength(uncompressedLength, true) -// val start = input.position() -// -// // have to adjust for uncompressed length-length -// length = length - lengthLength -// -// -// ///////// decompress data -// input.readBytes(temp, 0, length) -// -// -// // LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor) -// reader.reset() -// decompressor.decompress(temp, 0, reader.buffer, 0, uncompressedLength) -// reader.setLimit(uncompressedLength) -// if (DEBUG) { -// val compressed = Sys.bytesToHex(input.readAllBytes(), start, length) -// val orig = Sys.bytesToHex(reader.buffer, start, uncompressedLength) -// logger.error(OS.LINE_SEPARATOR + -// "COMPRESSED: (" + length + ")" + OS.LINE_SEPARATOR + compressed + -// OS.LINE_SEPARATOR + -// "ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig) -// } -// -// // read the object from the buffer. -// return read(connection, reader) -// } -// -// /** -// * BUFFER: -// * +++++++++++++++++++++++++++++++ -// * + IV (12) + encrypted data + -// * +++++++++++++++++++++++++++++++ -// * -// * ENCRYPTED DATA: -// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * + uncompressed length (1-4 bytes) + compressed data + -// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * -// * COMPRESSED DATA: -// * ++++++++++++++++++++++++++ -// * + class and object bytes + -// * ++++++++++++++++++++++++++ -// */ -// fun writeCrypto(logger: Logger, connection: Connection_, buffer: ByteBuf, message: Any) { -// // write the object to a TEMP buffer! this will be compressed later -// write(connection, writer, message) -// -// // save off how much data the object took -// val length = writer.position() -// val maxCompressedLength = compressor.maxCompressedLength(length) -// -// ////////// compressing data -// // we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger -// // output), will be negated by the increase in size by the encryption -// val compressOutput = temp -// -// // LZ4 compress. Offset by 4 in the dest array so we have room for the length -// val compressedLength = compressor.compress(writer.buffer, 0, length, compressOutput, 4, maxCompressedLength) -// if (DEBUG) { -// val orig = ByteBufUtil.hexDump(writer.buffer, 0, length) -// val compressed = ByteBufUtil.hexDump(compressOutput, 4, compressedLength) -// logger.error(OS.LINE_SEPARATOR + -// "ORIG: (" + length + ")" + OS.LINE_SEPARATOR + orig + -// OS.LINE_SEPARATOR + -// "COMPRESSED: (" + compressedLength + ")" + OS.LINE_SEPARATOR + compressed) -// } -// -// // now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version -// val lengthLength = OptimizeUtilsByteArray.intLength(length, true) -// -// // this is where we start writing the length data, so that the end of this lines up with the compressed data -// val start = 4 - lengthLength -// OptimizeUtilsByteArray.writeInt(compressOutput, length, true, start) -// -// // now compressOutput contains "uncompressed length + data" -// val compressedArrayLength = lengthLength + compressedLength -// -// -// /////// encrypting data. -// val cryptoKey = connection.cryptoKey() -// val iv = ByteArray(IV_LENGTH_BYTE) // NEVER REUSE THIS IV WITH SAME KEY -// secureRandom.nextBytes(iv) -// val parameterSpec = GCMParameterSpec(TAG_LENGTH_BIT, iv) // 128 bit auth tag length -// try { -// cipher!!.init(Cipher.ENCRYPT_MODE, cryptoKey, parameterSpec) -// } catch (e: Exception) { -// throw IOException("Unable to AES encrypt the data", e) -// } -// -// // we REUSE the writer buffer! (since that data is now compressed in a different array) -// val encryptedLength: Int -// encryptedLength = try { -// cipher!!.doFinal(compressOutput, start, compressedArrayLength, writer.buffer, 0) -// } catch (e: Exception) { -// throw IOException("Unable to AES encrypt the data", e) -// } -// -// // write out our IV -// buffer.writeBytes(iv, 0, IV_LENGTH_BYTE) -// Arrays.fill(iv, 0.toByte()) // overwrite the IV with zeros so we can't leak this value -// -// // have to copy over the orig data, because we used the temp buffer -// buffer.writeBytes(writer.buffer, 0, encryptedLength) -// if (DEBUG) { -// val ivString = ByteBufUtil.hexDump(iv, 0, IV_LENGTH_BYTE) -// val crypto = ByteBufUtil.hexDump(writer.buffer, 0, encryptedLength) -// logger.error(OS.LINE_SEPARATOR + -// "IV: (12)" + OS.LINE_SEPARATOR + ivString + -// OS.LINE_SEPARATOR + -// "CRYPTO: (" + encryptedLength + ")" + OS.LINE_SEPARATOR + crypto) -// } -// } - - /** - * BUFFER: - * +++++++++++++++++++++++++++++++ - * + IV (12) + encrypted data + - * +++++++++++++++++++++++++++++++ - * - * ENCRYPTED DATA: - * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - * + uncompressed length (1-4 bytes) + compressed data + - * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - * - * COMPRESSED DATA: - * ++++++++++++++++++++++++++ - * + class and object bytes + - * ++++++++++++++++++++++++++ - */ -// fun readCrypto(logger: Logger, connection: Connection_, buffer: ByteBuf, length: Int): Any { -// // read out the crypto IV -// var length = length -// val iv = ByteArray(IV_LENGTH_BYTE) -// buffer.readBytes(iv, 0, IV_LENGTH_BYTE) -// -// // have to adjust for the IV -// length = length - IV_LENGTH_BYTE -// -// /////////// decrypt data -// val cryptoKey = connection.cryptoKey() -// try { -// cipher!!.init(Cipher.DECRYPT_MODE, cryptoKey, GCMParameterSpec(TAG_LENGTH_BIT, iv)) -// } catch (e: Exception) { -// throw IOException("Unable to AES decrypt the data", e) -// } -// -// // have to copy out bytes, we reuse the reader byte array! -// buffer.readBytes(reader.buffer, 0, length) -// if (DEBUG) { -// val ivString = ByteBufUtil.hexDump(iv, 0, IV_LENGTH_BYTE) -// val crypto = ByteBufUtil.hexDump(reader.buffer, 0, length) -// logger.error("IV: (12)" + OS.LINE_SEPARATOR + ivString + -// OS.LINE_SEPARATOR + "CRYPTO: (" + length + ")" + OS.LINE_SEPARATOR + crypto) -// } -// val decryptedLength: Int -// decryptedLength = try { -// cipher!!.doFinal(reader.buffer, 0, length, temp, 0) -// } catch (e: Exception) { -// throw IOException("Unable to AES decrypt the data", e) -// } -// -// ///////// decompress data -- as it's ALWAYS compressed -// -// // get the decompressed length (at the beginning of the array) -// val uncompressedLength = OptimizeUtilsByteArray.readInt(temp, true) -// -// // where does our data start, AFTER the length field -// val start = OptimizeUtilsByteArray.intLength(uncompressedLength, true) // because 1-4 bytes for the uncompressed size; -// -// // LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor -// reader.reset() -// decompressor.decompress(temp, start, reader.buffer, 0, uncompressedLength) -// reader.setLimit(uncompressedLength) -// if (DEBUG) { -// val endWithoutUncompressedLength = decryptedLength - start -// val compressed = ByteBufUtil.hexDump(temp, start, endWithoutUncompressedLength) -// val orig = ByteBufUtil.hexDump(reader.buffer, 0, uncompressedLength) -// logger.error("COMPRESSED: (" + endWithoutUncompressedLength + ")" + OS.LINE_SEPARATOR + compressed + -// OS.LINE_SEPARATOR + -// "ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig) -// } -// -// // read the object from the buffer. -// return read(connection, reader) -// } -} diff --git a/src/dorkbox/network/serialization/KryoReader.kt b/src/dorkbox/network/serialization/KryoReader.kt new file mode 100644 index 00000000..9ffaf67b --- /dev/null +++ b/src/dorkbox/network/serialization/KryoReader.kt @@ -0,0 +1,326 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import dorkbox.network.connection.Connection +import org.agrona.DirectBuffer + +/** + * READ and WRITE are exclusive to each other and can be performed in different threads. + * + * snappycomp : 7.534 micros/op; 518.5 MB/s (output: 55.1%) + * snappyuncomp : 1.391 micros/op; 2808.1 MB/s + * lz4comp : 6.210 micros/op; 629.0 MB/s (output: 55.4%) + * lz4uncomp : 0.641 micros/op; 6097.9 MB/s + */ +class KryoReader(private val maxMessageSize: Int) : Kryo() { + companion object { + internal const val DEBUG = false + } + + // for kryo serialization + private val readerBuffer = AeronInput() + + // This is unique per connection. volatile/etc is not necessary because it is set/read in the same thread + lateinit var connection: CONNECTION + + +// // crypto + compression have to work with native byte arrays, so here we go... +// private val reader = Input(maxMessageSize) +// private val temp = ByteArray(maxMessageSize) +// +// private val cipher = Cipher.getInstance(CryptoManagement.AES_ALGORITHM)!! +// private val decompressor = LZ4Factory.fastestInstance().fastDecompressor()!! + + /** + * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! + * + * INPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + @Throws(Exception::class) + fun read(buffer: DirectBuffer): Any? { + // this properly sets the buffer info + readerBuffer.setBuffer(buffer, 0, buffer.capacity()) + return readClassAndObject(readerBuffer) + } + + /** + * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! + * + * INPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + @Throws(Exception::class) + fun read(buffer: DirectBuffer, offset: Int, length: Int): Any? { + // this properly sets the buffer info + readerBuffer.setBuffer(buffer, offset, length) + return readClassAndObject(readerBuffer) + } + + /** + * INPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + @Throws(Exception::class) + fun read(buffer: DirectBuffer, offset: Int, length: Int, connection: CONNECTION): Any? { + // required by RMI and some serializers to determine which connection wrote (or has info about) this object + this.connection = connection + + // this properly sets the buffer info + readerBuffer.setBuffer(buffer, offset, length) + return readClassAndObject(readerBuffer) + } + + + + + //////////////// + //////////////// + //////////////// + // for more complicated writes, sadly, we have to deal DIRECTLY with byte arrays + //////////////// + //////////////// + //////////////// + + /** + * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! + * + * INPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + fun read(reader: Input): Any? { + return readClassAndObject(reader) + } + + /** + * INPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + fun read(connection: CONNECTION, reader: Input): Any? { + // required by RMI and some serializers to determine which connection wrote (or has info about) this object + this.connection = connection + + return readClassAndObject(reader) + } + + + /** + * OUTPUT: + * ++++++++++++++++ + * + object bytes + + * ++++++++++++++++ + */ + fun readBytes(): ByteArray { + val dataLength = readerBuffer.readVarInt(true) + return readerBuffer.readBytes(dataLength) + } + +// /** +// * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! +// * +// * BUFFER: +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * + uncompressed length (1-4 bytes) + compressed data + +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * +// * COMPRESSED DATA: +// * ++++++++++++++++++++++++++ +// * + class and object bytes + +// * ++++++++++++++++++++++++++ +// */ +// fun readCompressed(logger: KLogger, input: Input, length: Int): Any? { +// //////////////// +// // Note: we CANNOT write BACK to the buffer as "temp" storage, since there could be additional data on it! +// //////////////// +// +// // get the decompressed length (at the beginning of the array) +// var length = length +// val uncompressedLength = input.readInt(true) +// if (uncompressedLength > (maxMessageSize*2)) { +// throw IOException("Uncompressed size ($uncompressedLength) is larger than max allowed size (${maxMessageSize*2})!") +// } +// +// // because 1-4 bytes for the decompressed size (this number is never negative) +// val lengthLength = OptimizeUtilsByteArray.intLength(uncompressedLength, true) +// val start = input.position() +// +// // have to adjust for uncompressed length-length +// length -= lengthLength +// +// +// ///////// decompress data +// input.readBytes(temp, 0, length) +// +// +// // LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor) +// reader.reset() +// decompressor.decompress(temp, 0, reader.buffer, 0, uncompressedLength) +// reader.setLimit(uncompressedLength) +// +// if (DEBUG) { +// val compressed = Sys.bytesToHex(temp, start, length) +// val orig = Sys.bytesToHex(reader.buffer, start, uncompressedLength) +// logger.error( +// OS.LINE_SEPARATOR + +// "COMPRESSED: (" + length + ")" + OS.LINE_SEPARATOR + compressed + +// OS.LINE_SEPARATOR + +// "ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig) +// } +// +// // read the object from the buffer. +// return read(reader) +// } +// +// /** +// * BUFFER: +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * + uncompressed length (1-4 bytes) + compressed data + +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * +// * COMPRESSED DATA: +// * ++++++++++++++++++++++++++ +// * + class and object bytes + +// * ++++++++++++++++++++++++++ +// */ +// fun readCompressed(logger: KLogger, connection: CONNECTION, input: Input, length: Int): Any? { +// //////////////// +// // Note: we CANNOT write BACK to the buffer as "temp" storage, since there could be additional data on it! +// //////////////// +// +// // get the decompressed length (at the beginning of the array) +// var length = length +// val uncompressedLength = input.readInt(true) +// if (uncompressedLength > (maxMessageSize*2)) { +// throw IOException("Uncompressed size ($uncompressedLength) is larger than max allowed size (${maxMessageSize*2})!") +// } +// +// // because 1-4 bytes for the decompressed size (this number is never negative) +// val lengthLength = OptimizeUtilsByteArray.intLength(uncompressedLength, true) +// val start = input.position() +// +// // have to adjust for uncompressed length-length +// length = length - lengthLength +// +// +// ///////// decompress data +// input.readBytes(temp, 0, length) +// +// +// // LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor) +// reader.reset() +// decompressor.decompress(temp, 0, reader.buffer, 0, uncompressedLength) +// reader.setLimit(uncompressedLength) +// if (DEBUG) { +// val compressed = Sys.bytesToHex(input.readAllBytes(), start, length) +// val orig = Sys.bytesToHex(reader.buffer, start, uncompressedLength) +// logger.error(OS.LINE_SEPARATOR + +// "COMPRESSED: (" + length + ")" + OS.LINE_SEPARATOR + compressed + +// OS.LINE_SEPARATOR + +// "ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig) +// } +// +// // read the object from the buffer. +// return read(connection, reader) +// } +// +// +// +// /** +// * BUFFER: +// * +++++++++++++++++++++++++++++++ +// * + IV (12) + encrypted data + +// * +++++++++++++++++++++++++++++++ +// * +// * ENCRYPTED DATA: +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * + uncompressed length (1-4 bytes) + compressed data + +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * +// * COMPRESSED DATA: +// * ++++++++++++++++++++++++++ +// * + class and object bytes + +// * ++++++++++++++++++++++++++ +// */ +// fun readCrypto(logger: KLogger, connection: CONNECTION, buffer: Input, length: Int): Any? { +// // read out the crypto IV +// var length = length +// val iv = ByteArray(CryptoManagement.GCM_IV_LENGTH_BYTES) +// buffer.readBytes(iv, 0, CryptoManagement.GCM_IV_LENGTH_BYTES) +// +// // have to adjust for the IV +// length -= CryptoManagement.GCM_IV_LENGTH_BYTES +// +// /////////// decrypt data +// try { +// cipher.init(Cipher.DECRYPT_MODE, connection.cryptoKey, GCMParameterSpec(CryptoManagement.GCM_TAG_LENGTH_BITS, iv)) +// } catch (e: Exception) { +// throw IOException("Unable to AES decrypt the data", e) +// } +// +// // have to copy out bytes, we reuse the reader byte array! +// buffer.readBytes(reader.buffer, 0, length) +// if (DEBUG) { +// val ivString = iv.toHexString() +// val crypto = reader.buffer.toHexString(length = length) +// logger.error("IV: (12)" + OS.LINE_SEPARATOR + ivString + +// OS.LINE_SEPARATOR + "CRYPTO: (" + length + ")" + OS.LINE_SEPARATOR + crypto) +// } +// +// val decryptedLength: Int +// decryptedLength = try { +// cipher.doFinal(reader.buffer, 0, length, temp, 0) +// } catch (e: Exception) { +// throw IOException("Unable to AES decrypt the data", e) +// } +// +// ///////// decompress data -- as it's ALWAYS compressed +// +// // get the decompressed length (at the beginning of the array) +// val uncompressedLength = OptimizeUtilsByteArray.readInt(temp, true) +// +// // where does our data start, AFTER the length field +// val start = OptimizeUtilsByteArray.intLength(uncompressedLength, true) // because 1-4 bytes for the uncompressed size; +// +// // LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor +// reader.reset() +// decompressor.decompress(temp, start, reader.buffer, 0, uncompressedLength) +// reader.setLimit(uncompressedLength) +// if (DEBUG) { +// val endWithoutUncompressedLength = decryptedLength - start +// val compressed = temp.toHexString(start = start, length = endWithoutUncompressedLength) +// val orig = reader.buffer.toHexString(length = uncompressedLength) +// logger.error("COMPRESSED: (" + endWithoutUncompressedLength + ")" + OS.LINE_SEPARATOR + compressed + +// OS.LINE_SEPARATOR + +// "ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig) +// } +// +// // read the object from the buffer. +// return read(connection, reader) +// } +} diff --git a/src/dorkbox/network/serialization/KryoWriter.kt b/src/dorkbox/network/serialization/KryoWriter.kt new file mode 100644 index 00000000..d3027856 --- /dev/null +++ b/src/dorkbox/network/serialization/KryoWriter.kt @@ -0,0 +1,303 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Output +import dorkbox.network.connection.Connection +import io.aeron.logbuffer.BufferClaim +import java.util.* + +/** + * READ and WRITE are exclusive to each other and can be performed in different threads. + * + * snappycomp : 7.534 micros/op; 518.5 MB/s (output: 55.1%) + * snappyuncomp : 1.391 micros/op; 2808.1 MB/s + * lz4comp : 6.210 micros/op; 629.0 MB/s (output: 55.4%) + * lz4uncomp : 0.641 micros/op; 6097.9 MB/s + * + */ +open class KryoWriter(maxMessageSize: Int) : Kryo() { + companion object { + internal const val DEBUG = false + } + + // for claiming a portion of the termBuffer in Aeron to write data to + internal val bufferClaim = BufferClaim() + + // for kryo serialization + internal val writerBuffer = AeronOutput(maxMessageSize) + + // This is unique per connection. volatile/etc is not necessary because it is set/read in the same thread + lateinit var connection: CONNECTION + +// // crypto + compression have to work with native byte arrays, so here we go... +// private val writer = Output(maxMessageSize) +// private val temp = ByteArray(maxMessageSize) +// +// private val cipher = Cipher.getInstance(CryptoManagement.AES_ALGORITHM)!! +// private val compressor = LZ4Factory.nativeInstance().fastCompressor() + + + /** + * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! + * + * OUTPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + @Throws(Exception::class) + fun write(message: Any): AeronOutput { + writerBuffer.reset() + writeClassAndObject(writerBuffer, message) + return writerBuffer + } + + /** + * OUTPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + @Throws(Exception::class) + fun write(connection: CONNECTION, message: Any): AeronOutput { + // required by RMI and some serializers to determine which connection wrote (or has info about) this object + this.connection = connection + + writerBuffer.reset() + writeClassAndObject(writerBuffer, message) + return writerBuffer + } + + //////////////// + //////////////// + //////////////// + // for more complicated writes, sadly, we have to deal DIRECTLY with byte arrays + //////////////// + //////////////// + //////////////// + + /** + * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! + * + * OUTPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + fun write(writer: Output, message: Any) { + // write the object to the NORMAL output buffer! + writer.reset() + writeClassAndObject(writer, message) + } + + /** + * OUTPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + private fun write(connection: CONNECTION, writer: Output, message: Any) { + // required by RMI and some serializers to determine which connection wrote (or has info about) this object + this.connection = connection + + // write the object to the NORMAL output buffer! + writer.reset() + writeClassAndObject(writer, message) + } + +// /** +// * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! +// * +// * BUFFER: +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * + uncompressed length (1-4 bytes) + compressed data + +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * +// * COMPRESSED DATA: +// * ++++++++++++++++++++++++++ +// * + class and object bytes + +// * ++++++++++++++++++++++++++ +// */ +// fun writeCompressed(logger: KLogger, output: Output, message: Any) { +// // write the object to a TEMP buffer! this will be compressed later +// write(writer, message) +// +// // save off how much data the object took +// val length = writer.position() +// val maxCompressedLength = compressor.maxCompressedLength(length) +// +// ////////// compressing data +// // we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger +// // output), will be negated by the increase in size by the encryption +// val compressOutput = temp +// +// +// // LZ4 compress. +// val compressedLength = compressor.compress(writer.buffer, 0, length, compressOutput, 0, maxCompressedLength) +// +// if (DEBUG) { +// val orig = writer.buffer.toHexString() +// val compressed = compressOutput.toHexString() +// logger.error( +// OS.LINE_SEPARATOR + +// "ORIG: (" + length + ")" + OS.LINE_SEPARATOR + orig + +// OS.LINE_SEPARATOR + +// "COMPRESSED: (" + compressedLength + ")" + OS.LINE_SEPARATOR + compressed) +// } +// +// // now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version +// output.writeInt(length, true) +// +// // have to copy over the orig data, because we used the temp buffer. Also have to account for the length of the uncompressed size +// output.writeBytes(compressOutput, 0, compressedLength) +// } +// +// /** +// * BUFFER: +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * + uncompressed length (1-4 bytes) + compressed data + +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * +// * COMPRESSED DATA: +// * ++++++++++++++++++++++++++ +// * + class and object bytes + +// * ++++++++++++++++++++++++++ +// */ +// fun writeCompressed(logger: KLogger, connection: CONNECTION, output: Output, message: Any) { +// // write the object to a TEMP buffer! this will be compressed later +// write(connection, writer, message) +// +// // save off how much data the object took +// val length = writer.position() +// val maxCompressedLength = compressor.maxCompressedLength(length) +// +// ////////// compressing data +// // we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger +// // output), will be negated by the increase in size by the encryption +// val compressOutput = temp +// +// // LZ4 compress. +// val compressedLength = compressor.compress(writer.buffer, 0, length, compressOutput, 0, maxCompressedLength) +// +// if (DEBUG) { +// val orig = writer.buffer.toHexString(length = length) +// val compressed = compressOutput.toHexString(length = compressedLength) +// logger.error("${OS.LINE_SEPARATOR}" + +// "ORIG: ($length) ${OS.LINE_SEPARATOR}" + +// "$orig ${OS.LINE_SEPARATOR}" + +// "COMPRESSED: ($compressedLength) ${OS.LINE_SEPARATOR} " + +// "$compressed") +// } +// +// // now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version +// output.writeInt(length, true) +// +// // have to copy over the orig data, because we used the temp buffer. Also have to account for the length of the uncompressed size +// output.writeBytes(compressOutput, 0, compressedLength) +// } +// +// +// +// /** +// * BUFFER: +// * +++++++++++++++++++++++++++++++ +// * + IV (12) + encrypted data + +// * +++++++++++++++++++++++++++++++ +// * +// * ENCRYPTED DATA: +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * + uncompressed length (1-4 bytes) + compressed data + +// * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * +// * COMPRESSED DATA: +// * ++++++++++++++++++++++++++ +// * + class and object bytes + +// * ++++++++++++++++++++++++++ +// */ +// fun writeCrypto(logger: KLogger, connection: CONNECTION, buffer: Output, message: Any) { +// // write the object to a TEMP buffer! this will be compressed later +// write(connection, writer, message) +// +// // save off how much data the object took +// val length = writer.position() +// val maxCompressedLength = compressor.maxCompressedLength(length) +// +// ////////// compressing data +// // we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger +// // output), will be negated by the increase in size by the encryption +// val compressOutput = temp +// +// // LZ4 compress. Offset by 4 in the dest array so we have room for the length +// val compressedLength = compressor.compress(writer.buffer, 0, length, compressOutput, 4, maxCompressedLength) +// if (DEBUG) { +// val orig = writer.buffer.toHexString(length = length) +// val compressed = compressOutput.toHexString(start = 4, length = compressedLength) +// logger.error(OS.LINE_SEPARATOR + +// "ORIG: (" + length + ")" + OS.LINE_SEPARATOR + orig + +// OS.LINE_SEPARATOR + +// "COMPRESSED: (" + compressedLength + ")" + OS.LINE_SEPARATOR + compressed) +// } +// +// // now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version +// val lengthLength = OptimizeUtilsByteArray.intLength(length, true) +// +// // this is where we start writing the length data, so that the end of this lines up with the compressed data +// val start = 4 - lengthLength +// OptimizeUtilsByteArray.writeInt(compressOutput, length, true, start) +// +// // now compressOutput contains "uncompressed length + data" +// val compressedArrayLength = lengthLength + compressedLength +// +// +// /////// encrypting data. +// val iv = ByteArray(CryptoManagement.GCM_IV_LENGTH_BYTES) // NEVER REUSE THIS IV WITH SAME KEY +// CryptoManagement.secureRandom.nextBytes(iv) +// +// val parameterSpec = GCMParameterSpec(CryptoManagement.GCM_TAG_LENGTH_BITS, iv) // 128 bit auth tag length +// +// try { +// cipher.init(Cipher.ENCRYPT_MODE, connection.cryptoKey, parameterSpec) +// } catch (e: Exception) { +// throw IOException("Unable to AES encrypt the data", e) +// } +// +// // we REUSE the writer buffer! (since that data is now compressed in a different array) +// val encryptedLength: Int +// encryptedLength = try { +// cipher.doFinal(compressOutput, start, compressedArrayLength, writer.buffer, 0) +// } catch (e: Exception) { +// throw IOException("Unable to AES encrypt the data", e) +// } +// +// // write out our IV +// buffer.writeBytes(iv, 0, CryptoManagement.GCM_IV_LENGTH_BYTES) +// Arrays.fill(iv, 0.toByte()) // overwrite the IV with zeros so we can't leak this value +// +// // have to copy over the orig data, because we used the temp buffer +// buffer.writeBytes(writer.buffer, 0, encryptedLength) +// +// if (DEBUG) { +// val ivString = iv.toHexString() +// val crypto = writer.buffer.toHexString(length = encryptedLength) +// logger.error(OS.LINE_SEPARATOR + +// "IV: (12)" + OS.LINE_SEPARATOR + ivString + +// OS.LINE_SEPARATOR + +// "CRYPTO: (" + encryptedLength + ")" + OS.LINE_SEPARATOR + crypto) +// } +// } +} diff --git a/src/dorkbox/network/serialization/Reserved.kt b/src/dorkbox/network/serialization/Reserved.kt new file mode 100644 index 00000000..2417bcdd --- /dev/null +++ b/src/dorkbox/network/serialization/Reserved.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.serialization + +/** + * These class registrations are so that we can ADD functionality to the system without having upstream implementations CHANGE + * their registration IDs. + * + * This permits us to have version A on one side of the connection, and version B (with a "new" class) on the other side. + * + * The only issue is that the new feature will NOT WORK until both sides have the same version. By running different versions, the upstream + * implementation will not break -- just the network feature will not work until both sides have it + */ +class Reserved0 {} +class Reserved1 {} +class Reserved2 {} +class Reserved3 {} +class Reserved4 {} +class Reserved5 {} +class Reserved6 {} +class Reserved7 {} +class Reserved8 {} +class Reserved9 {} diff --git a/src/dorkbox/network/serialization/SendSyncSerializer.kt b/src/dorkbox/network/serialization/SendSyncSerializer.kt new file mode 100644 index 00000000..39480c95 --- /dev/null +++ b/src/dorkbox/network/serialization/SendSyncSerializer.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import dorkbox.network.connection.SendSync + +internal class SendSyncSerializer: Serializer() { + override fun write(kryo: Kryo, output: Output, ssync: SendSync) { + output.writeInt(ssync.id) + kryo.writeClassAndObject(output, ssync.message) + } + + override fun read(kryo: Kryo, input: Input, type: Class): SendSync { + val ssync = SendSync() + ssync.id = input.readInt() + ssync.message = kryo.readClassAndObject(input) + + return ssync + } +} diff --git a/src/dorkbox/network/serialization/Serialization.kt b/src/dorkbox/network/serialization/Serialization.kt index 45439a15..07646d64 100644 --- a/src/dorkbox/network/serialization/Serialization.kt +++ b/src/dorkbox/network/serialization/Serialization.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,16 @@ package dorkbox.network.serialization import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.SerializerFactory -import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.serializers.DefaultSerializers +import com.esotericsoftware.kryo.serializers.ImmutableCollectionsSerializers import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy import com.esotericsoftware.minlog.Log import dorkbox.network.Server -import dorkbox.network.connection.CloseMessage import dorkbox.network.connection.Connection +import dorkbox.network.connection.DisconnectMessage +import dorkbox.network.connection.SendSync +import dorkbox.network.connection.buffer.BufferedMessages +import dorkbox.network.connection.buffer.BufferedSerializer import dorkbox.network.connection.streaming.StreamingControl import dorkbox.network.connection.streaming.StreamingControlSerializer import dorkbox.network.connection.streaming.StreamingData @@ -33,38 +37,39 @@ import dorkbox.network.ping.Ping import dorkbox.network.ping.PingSerializer import dorkbox.network.rmi.CachedMethod import dorkbox.network.rmi.RmiUtils -import dorkbox.network.rmi.messages.ConnectionObjectCreateRequest -import dorkbox.network.rmi.messages.ConnectionObjectCreateResponse -import dorkbox.network.rmi.messages.ConnectionObjectDeleteRequest -import dorkbox.network.rmi.messages.ConnectionObjectDeleteResponse -import dorkbox.network.rmi.messages.ContinuationSerializer -import dorkbox.network.rmi.messages.MethodRequest -import dorkbox.network.rmi.messages.MethodRequestSerializer -import dorkbox.network.rmi.messages.MethodResponse -import dorkbox.network.rmi.messages.MethodResponseSerializer -import dorkbox.network.rmi.messages.RmiClientSerializer -import dorkbox.network.rmi.messages.RmiServerSerializer +import dorkbox.network.rmi.messages.* +import dorkbox.objectPool.BoundedPoolObject import dorkbox.objectPool.ObjectPool import dorkbox.objectPool.Pool -import dorkbox.objectPool.PoolObject import dorkbox.os.OS -import dorkbox.serializers.SerializationDefaults +import dorkbox.serializers.* import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.atomic -import mu.KLogger -import mu.KotlinLogging -import org.agrona.DirectBuffer import org.agrona.collections.Int2ObjectHashMap import org.objenesis.instantiator.ObjectInstantiator import org.objenesis.strategy.StdInstantiatorStrategy +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import java.io.IOException import java.lang.reflect.Constructor import java.lang.reflect.InvocationHandler +import java.math.BigDecimal +import java.net.Inet4Address +import java.net.Inet6Address +import java.net.URI +import java.util.* +import java.util.regex.* import kotlin.coroutines.Continuation + +// Observability issues: make sure that we know WHAT connection is causing serialization errors when they occur! +// ASYC isues: RMI can timeout when OTHER rmi connections happen! EACH RMI NEEDS TO BE SEPARATE IN THE IO DISPATCHER + /** * Threads reading/writing at the same time a single instance of kryo. it is possible to use a single kryo with the use of - * synchronize, however - that defeats the point of having multi-threaded serialization. + * synchronize, however - that defeats the point of having multithreaded serialization. * * Additionally, this serialization manager will register the entire class+interface hierarchy for an object. If you want to specify a * serialization scheme for a specific class in an objects hierarchy, you must register that first. @@ -91,11 +96,14 @@ open class Serialization(private val references: Boolean init { Log.set(Log.LEVEL_ERROR) } + + val inet4AddressSerializer by lazy { Inet4AddressSerializer() } + val inet6AddressSerializer by lazy { Inet6AddressSerializer() } } open class RmiSupport internal constructor( private val initialized: AtomicBoolean, - private val classesToRegister: MutableList>, + private val classesToRegister: MutableList, private val rmiServerSerializer: RmiServerSerializer ) { /** @@ -127,14 +135,36 @@ open class Serialization(private val references: Boolean } } - private lateinit var logger: KLogger + private lateinit var logger: Logger + + @Volatile + private var maxMessageSize: Int = 500_000 + + private val writeKryos: Pool> = ObjectPool.nonBlockingBounded( + poolObject = object : BoundedPoolObject>() { + override fun newInstance(): KryoWriter { + return newWriteKryo() + } + }, + maxSize = OS.optimumNumberOfThreads * 2 + ) + private val readKryos: Pool> = ObjectPool.nonBlockingBounded( + poolObject = object : BoundedPoolObject>() { + override fun newInstance(): KryoReader { + return newReadKryo() + } + }, + maxSize = OS.optimumNumberOfThreads * 2 + ) + private var initialized = atomic(false) // used by operations performed during kryo initialization, which are by default package access (since it's an anon-inner class) // All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems. // Object checking is performed during actual registration. - private val classesToRegister = mutableListOf>() + private val classesToRegister = mutableListOf() + private lateinit var finalClassRegistrations: Array private lateinit var savedRegistrationDetails: ByteArray // the purpose of the method cache, is to accelerate looking up methods for specific class @@ -145,7 +175,7 @@ open class Serialization(private val references: Boolean // StdInstantiatorStrategy will create classes bypasses the constructor (which can be useful in some cases) THIS IS A FALLBACK! private val instantiatorStrategy = DefaultInstantiatorStrategy(StdInstantiatorStrategy()) - private val methodRequestSerializer = MethodRequestSerializer(methodCache) // note: the methodCache is configured BEFORE anything reads from it! + private val methodRequestSerializer = MethodRequestSerializer(methodCache) // the methodCache is configured BEFORE anything reads from it! private val methodResponseSerializer = MethodResponseSerializer() private val continuationSerializer = ContinuationSerializer() @@ -155,6 +185,14 @@ open class Serialization(private val references: Boolean private val streamingControlSerializer = StreamingControlSerializer() private val streamingDataSerializer = StreamingDataSerializer() private val pingSerializer = PingSerializer() + private val sendSyncSerializer = SendSyncSerializer() + private val disconnectSerializer = DisconnectSerializer() + private val bufferedMessageSerializer = BufferedSerializer() + + internal val fileContentsSerializer = FileContentsSerializer() + + + /** * There is additional overhead to using RMI. @@ -163,31 +201,13 @@ open class Serialization(private val references: Boolean * * This is NOT bi-directional. */ - val rmi: RmiSupport + val rmi = RmiSupport(initialized, classesToRegister, rmiServerSerializer) val rmiHolder = RmiHolder() // reflectASM doesn't work on android private val useAsm = !OS.isAndroid - // These are GLOBAL, single threaded only kryo instances. - // The readKryo WILL RE-CONFIGURED during the client handshake! (it is all the same thread, so object visibility is not a problem) - // NOTE: These following can ONLY be called on a single thread! - private var readKryo = initGlobalKryo() - - private val kryoPool: Pool> - - init { - val poolObject = object : PoolObject>() { - override fun newInstance(): KryoExtra { - return initKryo() - } - } - - kryoPool = ObjectPool.nonBlocking(poolObject) - rmi = RmiSupport(initialized, classesToRegister, rmiServerSerializer) - } - /** * Registers the class using the lowest, next available integer ID and the [default serializer][Kryo.getDefaultSerializer]. @@ -288,9 +308,9 @@ open class Serialization(private val references: Boolean } /** - * NOTE: When this fails, the CLIENT will just time out. We DO NOT want to send an error message to the client - * (it should check for updates or something else). We do not want to give "rogue" clients knowledge of the - * server, thus preventing them from trying to probe the server data structures. + * When this fails, the CLIENT will just time out. We DO NOT want to send an error message to the client + * (it should check for updates or something else). We do not want to give "rogue" clients knowledge of the + * server, thus preventing them from trying to probe the server data structures. * * @return a compressed byte array of the details of all registration IDs -> Class name -> Serialization type used by kryo */ @@ -301,9 +321,8 @@ open class Serialization(private val references: Boolean /** * Kryo specifically for handshakes */ - internal fun initHandshakeKryo(): KryoExtra { - val kryo = KryoExtra() - + internal fun newHandshakeKryo(kryo: Kryo) { + // All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems. kryo.instantiatorStrategy = instantiatorStrategy kryo.references = references @@ -311,21 +330,22 @@ open class Serialization(private val references: Boolean kryo.setDefaultSerializer(factory) } - // All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems. - SerializationDefaults.register(kryo) - + kryo.register(ByteArray::class.java) kryo.register(HandshakeMessage::class.java) - - return kryo } /** - * called as the first thing inside when initializing the classesToRegister - */ - private fun initGlobalKryo(): KryoExtra { - // NOTE: classesToRegister.forEach will be called after serialization init! + * called as the first thing inside when initializing the classesToRegister + */ + private fun newGlobalKryo(kryo: Kryo) { + // NOTE: classesRegistrations.forEach will be called after serialization init! + // NOTE: All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems. + - val kryo = KryoExtra() + // WIP Java17 + // Serializes objects using Java's built in serialization mechanism. + // Note that this is very inefficient and should be avoided if possible. +// val javaSerializer = JavaSerializer() kryo.instantiatorStrategy = instantiatorStrategy kryo.references = references @@ -334,24 +354,81 @@ open class Serialization(private val references: Boolean kryo.setDefaultSerializer(factory) } - // All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems. - SerializationDefaults.register(kryo) - -// serialization.register(PingMessage::class.java) // TODO this is built into aeron!??!?!?! - - // TODO: this is for diffie hellmen handshake stuff! -// serialization.register(IESParameters::class.java, IesParametersSerializer()) -// serialization.register(IESWithCipherParameters::class.java, IesWithCipherParametersSerializer()) - // TODO: fix kryo to work the way we want, so we can register interfaces + serializers with kryo -// serialization.register(XECPublicKey::class.java, XECPublicKeySerializer()) -// serialization.register(XECPrivateKey::class.java, XECPrivateKeySerializer()) -// serialization.register(Message::class.java) // must use full package name! + // wip java 17 serialization +// kryo.addDefaultSerializer(Throwable::class.java, javaSerializer) // this doesn't work properly! + + // these are registered using the default serializers. We don't customize these, because we don't care about it. + kryo.register(String::class.java) + kryo.register(Array::class.java) + kryo.register(IntArray::class.java) + kryo.register(ShortArray::class.java) + kryo.register(FloatArray::class.java) + kryo.register(DoubleArray::class.java) + kryo.register(LongArray::class.java) + kryo.register(ByteArray::class.java) + kryo.register(CharArray::class.java) + kryo.register(BooleanArray::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array>::class.java) + kryo.register(Class::class.java) + + kryo.register(Exception::class.java) + kryo.register(IOException::class.java) + kryo.register(RuntimeException::class.java) + kryo.register(NullPointerException::class.java) + + kryo.register(BigDecimal::class.java) + kryo.register(BitSet::class.java) + + // necessary for the transport of exceptions. + kryo.register(StackTraceElement::class.java) + kryo.register(Array::class.java) + kryo.register(ArrayList::class.java) + kryo.register(HashMap::class.java) + kryo.register(HashSet::class.java) + + kryo.register(EnumSet::class.java, DefaultSerializers.EnumSetSerializer()) + kryo.register(EnumMap::class.java, EnumMapSerializer()) + kryo.register(Arrays.asList("").javaClass, DefaultSerializers.ArraysAsListSerializer()) + + kryo.register(emptyList().javaClass) + kryo.register(emptySet().javaClass) + kryo.register(emptyMap().javaClass) + kryo.register(Collections.EMPTY_LIST.javaClass) + kryo.register(Collections.EMPTY_SET.javaClass) + kryo.register(Collections.EMPTY_MAP.javaClass) + + kryo.register(Collections.emptyNavigableSet().javaClass) + kryo.register(Collections.emptyNavigableMap().javaClass) + + kryo.register(Collections.singletonMap("", "").javaClass, DefaultSerializers.CollectionsSingletonMapSerializer()) + kryo.register(listOf("").javaClass, DefaultSerializers.CollectionsSingletonListSerializer()) + kryo.register(setOf("").javaClass, DefaultSerializers.CollectionsSingletonSetSerializer()) + + kryo.register(Pattern::class.java, RegexSerializer()) + kryo.register(URI::class.java, DefaultSerializers.URISerializer()) + kryo.register(UUID::class.java, DefaultSerializers.UUIDSerializer()) + + kryo.register(Inet4Address::class.java, inet4AddressSerializer) + kryo.register(Inet6Address::class.java, inet6AddressSerializer) + kryo.register(File::class.java, fileContentsSerializer) + + ImmutableCollectionsSerializers.registerSerializers(kryo) + UnmodifiableCollectionsSerializer.registerSerializers(kryo) + SynchronizedCollectionsSerializer.registerSerializers(kryo) // RMI stuff! kryo.register(ConnectionObjectCreateRequest::class.java) kryo.register(ConnectionObjectCreateResponse::class.java) kryo.register(ConnectionObjectDeleteRequest::class.java) - kryo.register(ConnectionObjectDeleteResponse::class.java) kryo.register(MethodRequest::class.java, methodRequestSerializer) kryo.register(MethodResponse::class.java, methodResponseSerializer) @@ -361,81 +438,133 @@ open class Serialization(private val references: Boolean kryo.register(StreamingData::class.java, streamingDataSerializer) kryo.register(Ping::class.java, pingSerializer) + kryo.register(SendSync::class.java, sendSyncSerializer) kryo.register(HandshakeMessage::class.java) - kryo.register(CloseMessage::class.java) + kryo.register(DisconnectMessage::class.java, disconnectSerializer) + kryo.register(BufferedMessages::class.java, bufferedMessageSerializer) + @Suppress("UNCHECKED_CAST") kryo.register(InvocationHandler::class.java as Class, rmiClientSerializer) - kryo.register(Continuation::class.java, continuationSerializer) - return kryo + kryo.register(Reserved0::class.java) + kryo.register(Reserved1::class.java) + kryo.register(Reserved2::class.java) + kryo.register(Reserved3::class.java) + kryo.register(Reserved4::class.java) + kryo.register(Reserved5::class.java) + kryo.register(Reserved6::class.java) + kryo.register(Reserved7::class.java) + kryo.register(Reserved8::class.java) + kryo.register(Reserved9::class.java) } /** - * called as the first thing inside when initializing the classesToRegister + * Called during EndPoint initialization + * + * This is to prevent (and recognize) out-of-order class/serializer registration. If an ID is already in use by a different type, an exception is thrown. */ - private fun initKryo(): KryoExtra { - val kryo = initGlobalKryo() + internal fun finishInit(type: Class<*>, maxMessageSize: Int) { + logger = LoggerFactory.getLogger(type.simpleName) + this.maxMessageSize = maxMessageSize - // check to see which interfaces are mapped to RMI (otherwise, the interface requires a serializer) - // note, we have to check to make sure a class is not ALREADY registered for RMI before it is registered again - classesToRegister.forEach { registration -> - registration.register(kryo, rmiHolder) - } + logger.info("UDP frame size: $maxMessageSize") - return kryo - } + val firstInitialization = initialized.compareAndSet(expect = false, update = true) + if (type == Server::class.java) { + if (!firstInitialization) { + throw IllegalArgumentException("Unable to initialize object serialization more than once!") + } + + // DO NOT USE THE POOL! This kryo instance must be thrown away! + val kryo = KryoWriter(maxMessageSize) + newGlobalKryo(kryo) + + initializeRegistrations(kryo, classesToRegister) + classesToRegister.clear() // don't need to keep a reference, since this can never be reinitialized. + + if (logger.isTraceEnabled) { + logger.trace("Registered classes for serialization:") + + // log the in-order output first + finalClassRegistrations.forEach { classRegistration -> + logger.trace("\t${classRegistration.info}") + } + } + } else { + // the client CAN initialize more than once, HOWEVER initialization happens in the handshake and this is explicitly permitted + } + } /** - * Called when server initialization is complete. - * Called when client connection receives kryo registration details + * Called when client connection receives kryo registration details. + * + * This is called BEFORE the connection object is created + * + * NOTE: to be clear, the "client" can ONLY registerRmi(IFACE, IMPL), to have extra info as the RMI-SERVER + * the client DOES NOT need to register anything else! It will register what the server sends. * * This is to prevent (and recognize) out-of-order class/serializer registration. If an ID is already in use by a different type, an exception is thrown. + * + * @return true if initialization was successful, false otherwise. DOES NOT CATCH EXCEPTIONS EXTERNALLY */ - internal fun finishInit(type: Class<*>, kryoRegistrationDetailsFromServer: ByteArray = ByteArray(0)): Boolean { + internal fun finishClientConnect(kryoRegistrationDetailsFromServer: ByteArray) { + val readKryo = KryoReader(maxMessageSize) + // DO NOT USE THE POOL! This kryo instance must be thrown away! + val writeKryo = KryoWriter(maxMessageSize) + newGlobalKryo(readKryo) + newGlobalKryo(writeKryo) - logger = KotlinLogging.logger(type.simpleName) + // we self initialize our registrations, THEN we compare them to the server. + val newRegistrations = initializeRegistrationsForClient(kryoRegistrationDetailsFromServer, classesToRegister, readKryo) + ?: throw Exception("Unable to initialize class registration information from the server") - // this will set up the class registration information - return if (type == Server::class.java) { - if (!initialized.compareAndSet(expect = false, update = true)) { - require(false) { "Unable to initialize serialization more than once!" } - return false - } + initializeRegistrations(writeKryo, newRegistrations) - val kryo = initKryo() - initializeClassRegistrations(kryo) - } else { - if (!initialized.compareAndSet(expect = false, update = true)) { - // the client CAN initialize more than once, since initialization happens in the handshake now - return true - } + // NOTE: we MUST be super careful to never modify `classesToRegister`!! + // NOTE: DO NOT CLEAR THIS WITH CLIENTS, THEY HAVE TO REBUILD EVERY TIME WITH A NEW CONNECTION! + // classesToRegister.clear() // don't need to keep a reference, since this can never be reinitialized. - // we have to allow CUSTOM classes to register (where the order does not matter), so that if the CLIENT is the RMI-SERVER, it can - // specify IMPL classes for RMI. - classesToRegister.forEach { registration -> - require(registration is ClassRegistrationForRmi) { "Unable to register a *class* by itself. This is only permitted on the CLIENT for RMI. " + - "To fix this, remove xx.register(${registration.clazz.name})" } + if (logger.isTraceEnabled) { + // log the in-order output first + finalClassRegistrations.forEach { classRegistration -> + logger.trace(classRegistration.info) } + } + } - @Suppress("UNCHECKED_CAST") - val classesToRegisterForRmi = listOf(*classesToRegister.toTypedArray()) as List> - classesToRegister.clear() - // NOTE: to be clear, the "client" can ONLY registerRmi(IFACE, IMPL), to have extra info as the RMI-SERVER!! - // the client DOES NOT need to register anything! It will register what the server sends. + /** + * @throws IllegalArgumentException if there is are too many RMI methods OR if a problem setting up the registration details + */ + private fun initializeRegistrations(kryo: KryoWriter, classesToRegister: List) { + val mergedRegistrations = mergeClassRegistrations(classesToRegister, kryo) - val kryo = initKryo() // this will initialize the class registrations - initializeClient(kryoRegistrationDetailsFromServer, classesToRegisterForRmi, kryo) - } + // make sure our RMI cached methods have been initialized + initializeRmiMethodCache(mergedRegistrations, kryo) + + // we can use any kryo, as long as that kryo is registered properly + savedRegistrationDetails = createRegistrationDetails(mergedRegistrations, kryo) + + finalClassRegistrations = mergedRegistrations.toTypedArray() } - private fun initializeClassRegistrations(kryo: KryoExtra): Boolean { - // now MERGE all of the registrations (since we can have registrations overwrite newer/specific registrations based on ID + + /** + * Merge all the registrations (since we can have registrations overwrite newer/specific registrations based on ID) + */ + private fun mergeClassRegistrations(classesToRegister: List, kryo: Kryo): List { + + // check to see which interfaces are mapped to RMI (otherwise, the interface requires a serializer) + // note, we have to check to make sure a class is not ALREADY registered for RMI before it is registered again + classesToRegister.forEach { registration -> + registration.register(kryo, rmiHolder) + } + // in order to get the ID's, these have to be registered with a kryo instance! - val mergedRegistrations = mutableListOf>() + val mergedRegistrations = mutableListOf() classesToRegister.forEach { registration -> val id = registration.id @@ -466,33 +595,22 @@ open class Serialization(private val references: Boolean mergedRegistrations.sortBy { it.id } - // now all of the registrations are IN ORDER and MERGED (save back to original array) - - - // set 'classesToRegister' to our mergedRegistrations, because this is now the correct order - classesToRegister.clear() - classesToRegister.addAll(mergedRegistrations) - - - // now create the registration details, used to validate that the client/server have the EXACT same class registration setup - val registrationDetails = arrayListOf>() - - if (logger.isTraceEnabled) { - // log the in-order output first - classesToRegister.forEach { classRegistration -> - logger.trace(classRegistration.info) - } - } + // now all the registrations are IN ORDER and MERGED (save back to original array) + return mergedRegistrations + } + /** + * This allows us to cache the relevant RMI methods + * + * @throws IllegalArgumentException if there are too many RMI methods + */ + private fun initializeRmiMethodCache(classesToRegister: List, kryo: Kryo) { classesToRegister.forEach { classRegistration -> - // now save all of the registration IDs for quick verification/access - registrationDetails.add(classRegistration.getInfoArray()) - // we should cache RMI methods! We don't always know if something is RMI or not (from just how things are registered...) // so it is super trivial to map out all possible, relevant types val kryoId = classRegistration.id - if (classRegistration is ClassRegistrationForRmi) { + if (classRegistration is ClassRegistrationForRmi<*>) { // on the "RMI server" (aka, where the object lives) side, there will be an interface + implementation! val implClass = classRegistration.implClass @@ -502,36 +620,38 @@ open class Serialization(private val references: Boolean // server // RMI-server method caching - methodCache[kryoId] = - RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, implClass, kryoId) + methodCache[kryoId] = RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, implClass, kryoId) // we ALSO have to cache the instantiator for these, since these are used to create remote objects @Suppress("UNCHECKED_CAST") - rmiHolder.idToInstantiator[kryoId] = - kryo.instantiatorStrategy.newInstantiatorOf(implClass) as ObjectInstantiator + rmiHolder.idToInstantiator[kryoId] = kryo.instantiatorStrategy.newInstantiatorOf(implClass) as ObjectInstantiator } else { // client // RMI-client method caching - methodCache[kryoId] = - RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, null, kryoId) + methodCache[kryoId] = RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, null, kryoId) } } else if (classRegistration.clazz.isInterface) { // non-RMI method caching - methodCache[kryoId] = - RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, null, kryoId) + methodCache[kryoId] = RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, null, kryoId) } if (kryoId >= 65535) { - throw RuntimeException("There are too many kryo class registrations!!") + throw IllegalArgumentException("There are too many kryo class registrations!!") } } + } + /** + * @throws IllegalArgumentException if there is a problem setting up the registration details + */ + private fun createRegistrationDetails(classesToRegister: List, kryo: KryoWriter): ByteArray { + // now create the registration details, used to validate that the client/server have the EXACT same class registration setup + val registrationDetails = arrayListOf>() - // we have to check to make sure all classes are registered on the GLOBAL READ KRYO !!! - // Because our classes are registered LAST, this will always be correct. - classesToRegister.forEach { registration -> - registration.register(readKryo, rmiHolder) + // now save all the registration IDs for quick verification/access + classesToRegister.forEach { classRegistration -> + registrationDetails.add(classRegistration.getInfoArray()) } // save this as a byte array (so class registration validation during connection handshake is faster) @@ -539,26 +659,41 @@ open class Serialization(private val references: Boolean try { kryo.write(output, registrationDetails.toTypedArray()) } catch (e: Exception) { - logger.error("Unable to write compressed data for registration details", e) - return false + throw IllegalArgumentException("Unable to write compressed data for registration details", e) } val length = output.position() - savedRegistrationDetails = ByteArray(length) + val savedRegistrationDetails = ByteArray(length) output.toBytes().copyInto(savedRegistrationDetails, 0, 0, length) output.close() - return true + return savedRegistrationDetails } + /** + * @return false will HARD FAIL the client. The server ignores the return NOTE: THIS IS BE EXCEPTION FREE! + */ @Suppress("UNCHECKED_CAST") - private fun initializeClient(kryoRegistrationDetailsFromServer: ByteArray, - classesToRegisterForRmi: List>, - kryo: KryoExtra): Boolean { + private fun initializeRegistrationsForClient( + kryoRegistrationDetailsFromServer: ByteArray, classesToRegister: List, kryo: KryoReader + ): MutableList? { + + // we have to allow CUSTOM classes to register (where the order does not matter), so that if the CLIENT is the RMI-SERVER, it can + // specify IMPL classes for RMI. + classesToRegister.forEach { registration -> + require(registration is ClassRegistrationForRmi<*>) { "Unable to register a *class* by itself. This is only permitted on the CLIENT for RMI. " + + "To fix this, remove xx.register(${registration.clazz.name})" } + } + + @Suppress("UNCHECKED_CAST") + val classesToRegisterForRmi = listOf(*classesToRegister.toTypedArray()) as List> + val input = AeronInput(kryoRegistrationDetailsFromServer) val clientClassRegistrations = kryo.read(input) as Array> + val newRegistrations = mutableListOf() val maker = kryo.instantiatorStrategy + val rmiSerializer = rmiServerSerializer try { // note: this list will be in order by ID! @@ -584,10 +719,30 @@ open class Serialization(private val references: Boolean } when (typeId) { - 0 -> classesToRegister.add(ClassRegistration0(clazz, maker.newInstantiatorOf(Class.forName(serializerName)).newInstance() as Serializer)) - 1 -> classesToRegister.add(ClassRegistration1(clazz, id)) - 2 -> classesToRegister.add(ClassRegistration2(clazz, maker.newInstantiatorOf(Class.forName(serializerName)).newInstance() as Serializer, id)) - 3 -> classesToRegister.add(ClassRegistration3(clazz)) + 0 -> { + if (logger.isTraceEnabled) { + logger.trace("REGISTRATION (0) ${clazz.name}") + } + newRegistrations.add(ClassRegistration0(clazz, maker.newInstantiatorOf(Class.forName(serializerName)).newInstance() as Serializer)) + } + 1 -> { + if (logger.isTraceEnabled) { + logger.trace("REGISTRATION (1) ${clazz.name} :: $id") + } + newRegistrations.add(ClassRegistration1(clazz, id)) + } + 2 -> { + if (logger.isTraceEnabled) { + logger.trace("REGISTRATION (2) ${clazz.name} :: $id") + } + newRegistrations.add(ClassRegistration2(clazz, maker.newInstantiatorOf(Class.forName(serializerName)).newInstance() as Serializer, id)) + } + 3 -> { + if (logger.isTraceEnabled) { + logger.trace("REGISTRATION (3) ${clazz.name}") + } + newRegistrations.add(ClassRegistration3(clazz)) + } 4 -> { // NOTE: when reconstructing, if we have access to the IMPL, we use it. WE MIGHT NOT HAVE ACCESS TO IT ON THE CLIENT! // we literally want everything to be 100% the same. @@ -606,16 +761,17 @@ open class Serialization(private val references: Boolean // NOTE: implClass can still be null! - logger.trace { - if (implClass != null) { - "REGISTRATION (RMI-CLIENT) ${clazz.name} -> ${implClass.name}" - } else { - "REGISTRATION (RMI-CLIENT) ${clazz.name}" - } + if (logger.isTraceEnabled) { + logger.trace( + if (implClass != null) { + "REGISTRATION (RMI-CLIENT) ${clazz.name} -> ${implClass.name}" + } else { + "REGISTRATION (RMI-CLIENT) ${clazz.name}" + } + ) } - classesToRegister.add(ClassRegistrationForRmi(clazz, implClass, rmiServerSerializer)) - + newRegistrations.add(ClassRegistrationForRmi(clazz, implClass, rmiSerializer)) } else -> throw IllegalStateException("Unable to manage class registrations for unknown registration type $typeId") } @@ -624,31 +780,60 @@ open class Serialization(private val references: Boolean } } catch (e: Exception) { logger.error("Error creating client class registrations using server data!", e) - return false + return null } - // so far, our CURRENT kryo instance was 'registered' with everything, EXCEPT our classesToRegister. - // fortunately for us, this always happens LAST, so we can "do it" here instead of having to reInit kryo all over - classesToRegister.forEach { registration -> - registration.register(kryo, rmiHolder) - } + return newRegistrations + } + + fun take(): KryoWriter { + return writeKryos.take() + } + + fun put(kryo: KryoWriter) { + writeKryos.put(kryo) + } + + fun takeRead(): KryoReader { + return readKryos.take() + } - // now do a round-trip through the class registrations - return initializeClassRegistrations(kryo) + fun putRead(kryo: KryoReader) { + readKryos.put(kryo) } + /** + * NOTE: A kryo instance CANNOT be re-used until after it's buffer is flushed to the network! + * * @return takes a kryo instance from the pool, or creates one if the pool was empty */ - fun takeKryo(): KryoExtra { - return kryoPool.take() - } + fun newReadKryo(): KryoReader { + val kryo = KryoReader(maxMessageSize) + newGlobalKryo(kryo) + + // the final list of all registrations in the EndPoint. This cannot change for the serer. + finalClassRegistrations.forEach { registration -> + registration.register(kryo, rmiHolder) + } + return kryo + } /** - * Returns a kryo instance to the pool for re-use later on + * NOTE: A kryo instance CANNOT be re-used until after it's buffer is flushed to the network! + * + * @return takes a kryo instance from the pool, or creates one if the pool was empty */ - fun returnKryo(kryo: KryoExtra) { - kryoPool.put(kryo) + private fun newWriteKryo(): KryoWriter { + val kryo = KryoWriter(maxMessageSize) + newGlobalKryo(kryo) + + // the final list of all registrations in the EndPoint. This cannot change for the serer. + finalClassRegistrations.forEach { registration -> + registration.register(kryo, rmiHolder) + } + + return kryo } /** @@ -726,15 +911,6 @@ open class Serialization(private val references: Boolean return methodCache[classId] } - // NOTE: These following functions are ONLY called on a single thread! - fun readMessage(buffer: DirectBuffer, offset: Int, length: Int, connection: CONNECTION): Any? { - return readKryo.read(buffer, offset, length, connection) - } - - fun readRaw(): Input { - return readKryo.readerBuffer - } - // /** // * # BLOCKING diff --git a/src/dorkbox/network/serialization/SettingsStore.kt b/src/dorkbox/network/serialization/SettingsStore.kt index dd6d02a3..0dd4fa7b 100644 --- a/src/dorkbox/network/serialization/SettingsStore.kt +++ b/src/dorkbox/network/serialization/SettingsStore.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,48 +21,47 @@ import dorkbox.netUtil.IP import dorkbox.netUtil.IPv4 import dorkbox.netUtil.IPv6 import dorkbox.network.connection.CryptoManagement -import dorkbox.serializers.SerializationDefaults import dorkbox.storage.Storage -import mu.KLogger +import dorkbox.storage.serializer.SerializerBytes +import org.slf4j.Logger import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress -import java.security.SecureRandom /** * This class provides a way for the network stack to use a database of some sort. */ @Suppress("unused") -class SettingsStore(storageBuilder: Storage.Builder, val logger: KLogger) : AutoCloseable { +class SettingsStore(storageBuilder: Storage.Builder, val logger: Logger) : AutoCloseable { companion object { /** * Address 0.0.0.0 or ::0 may be used as a source address for this host on this network. * * Because we assigned BOTH to the same thing, it doesn't REALLY matter which one we use, so we use BOTH! */ - internal val local4Buffer = IPv4.WILDCARD - internal val local6Buffer = IPv6.WILDCARD + private val local4Buffer = IPv4.WILDCARD + private val local6Buffer = IPv6.WILDCARD - internal const val saltKey = "_salt" - internal const val privateKey = "_private" + private const val saltKey = "_salt" + private const val privateKey_ = "_private" } - val store: Storage + private val store: Storage init { store = storageBuilder.logger(logger).apply { if (isStringBased) { // have to load/save keys+values as strings - onLoad { key, value, load -> - // key/value will be strings for a string based storage system + onLoad { _, key, value, load -> + // key/value will ALWAYS be strings for a string based storage system key as String value as String // we want the keys to be easy to read in case we are using string based storage val xKey: Any? = when (key) { - saltKey, privateKey -> key + saltKey, privateKey_ -> key else -> { IP.toAddress(key) } @@ -75,10 +74,10 @@ class SettingsStore(storageBuilder: Storage.Builder, val logger: KLogger) : Auto val xValue = value.decodeBase58() load(xKey, xValue) - }.onSave { key, value, save -> + }.onSave { _, key, value, save -> // we want the keys to be easy to read in case we are using string based storage val xKey = when (key) { - saltKey, privateKey, Storage.versionTag -> key + saltKey, privateKey_, Storage.versionTag -> key is InetAddress -> IP.toString(key) else -> null } @@ -104,11 +103,11 @@ class SettingsStore(storageBuilder: Storage.Builder, val logger: KLogger) : Auto } } else { // everything is stored as bytes. We use a serializer instead to register types for easy serialization - onNewSerializer { + serializer(SerializerBytes { register(ByteArray::class.java) - register(Inet4Address::class.java, SerializationDefaults.inet4AddressSerializer) - register(Inet6Address::class.java, SerializationDefaults.inet6AddressSerializer) - } + register(Inet4Address::class.java, Serialization.inet4AddressSerializer) + register(Inet6Address::class.java, Serialization.inet6AddressSerializer) + }) } }.build() @@ -116,60 +115,55 @@ class SettingsStore(storageBuilder: Storage.Builder, val logger: KLogger) : Auto // have to init salt val currentValue: ByteArray? = store[saltKey] if (currentValue == null) { - val secureRandom = SecureRandom() - // server salt is used to salt usernames and other various connection handshake parameters val bytes = ByteArray(32) // same size as our public/private key info - secureRandom.nextBytes(bytes) + CryptoManagement.secureRandom.nextBytes(bytes) // have to explicitly set it (so it will save) store[saltKey] = bytes } } - - /** - * @return the private key of the server - * - * @throws SecurityException - */ - fun getPrivateKey(): ByteArray? { - checkAccess(CryptoManagement::class.java) - return store[privateKey] - } - /** - * Saves the private key of the server - * - * @throws SecurityException + * @return true if both the private and public keys are non-null */ - fun savePrivateKey(serverPrivateKey: ByteArray) { - store[privateKey] = serverPrivateKey + fun validKeys(): Boolean { + val pubKey = store.get(local4Buffer) as ByteArray? + val privKey = store.get(privateKey_) as ByteArray? + return pubKey != null && privKey != null } /** - * @return the public key of the server + * the private key of the server * * @throws SecurityException */ - fun getPublicKey(): ByteArray? { - return store[local4Buffer] - } + var privateKey: ByteArray + get() { + checkAccess(CryptoManagement::class.java) + return store[privateKey_]!! + } + set(value) { + store[privateKey_] = value + } /** - * Saves the public key of the server + * the public key of the server * * @throws SecurityException */ - fun savePublicKey(serverPublicKey: ByteArray) { - store[local4Buffer] = serverPublicKey - store[local6Buffer] = serverPublicKey - } + var publicKey: ByteArray + get() { return store[local4Buffer]!! } + set(value) { + store[local4Buffer] = value + store[local6Buffer] = value + } /** * @return the server salt */ - fun getSalt(): ByteArray { + val salt: ByteArray + get() { return store[saltKey]!! } @@ -198,7 +192,13 @@ class SettingsStore(storageBuilder: Storage.Builder, val logger: KLogger) : Auto * Take the proper steps to close the storage system. */ override fun close() { - store.close() + logger.debug("Closing storage...") + try { + store.close() + } + catch (exception: Exception) { + logger.error("Unable to close the storage!", exception) + } } @@ -425,4 +425,6 @@ class SettingsStore(storageBuilder: Storage.Builder, val logger: KLogger) : Auto } return true } + + } diff --git a/src/dorkbox/network/serialization/package-info.java b/src/dorkbox/network/serialization/package-info.java new file mode 100644 index 00000000..38d4ec21 --- /dev/null +++ b/src/dorkbox/network/serialization/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkbox.network.serialization; diff --git a/src/module-info.java.NOT_SUPPORTED b/src/module-info.java similarity index 81% rename from src/module-info.java.NOT_SUPPORTED rename to src/module-info.java index 46295c0e..0420371c 100644 --- a/src/module-info.java.NOT_SUPPORTED +++ b/src/module-info.java @@ -3,8 +3,8 @@ exports dorkbox.network.aeron; exports dorkbox.network.connection; exports dorkbox.network.connection.streaming; + exports dorkbox.network.connection.buffer; exports dorkbox.network.connectionType; - exports dorkbox.network.coroutines; exports dorkbox.network.exceptions; exports dorkbox.network.handshake; exports dorkbox.network.ipFilter; @@ -12,8 +12,11 @@ exports dorkbox.network.rmi; exports dorkbox.network.serialization; - requires transitive dorkbox.bytes; + requires transitive dorkbox.byteUtils; + requires transitive dorkbox.classUtils; requires transitive dorkbox.collections; + requires transitive dorkbox.dns; + requires transitive dorkbox.hexUtils; requires transitive dorkbox.updates; requires transitive dorkbox.utilities; requires transitive dorkbox.netutil; @@ -24,24 +27,23 @@ requires transitive dorkbox.os; requires transitive expiringmap; - requires net.jodah.typetools; requires transitive com.esotericsoftware.kryo; requires transitive com.esotericsoftware.reflectasm; requires transitive org.objenesis; - requires io.aeron.all; - // requires io.aeron.driver; - // requires io.aeron.client; - // requires org.agrona.core; + // requires transitive io.aeron.all; // only needed when debugging builds + requires io.aeron.driver; + requires io.aeron.client; + requires org.agrona.core; requires transitive org.slf4j; - requires kotlin.logging.jvm; requires transitive kotlinx.atomicfu; requires kotlin.stdlib; - requires kotlin.stdlib.jdk8; - requires kotlinx.coroutines.core.jvm; + requires kotlinx.coroutines.core; + + // requires kotlinx.coroutines.core.jvm; // compile-only libraries // requires static chronicle.map; @@ -75,6 +77,5 @@ // requires static jnr.ffi; - requires annotations; - requires java.base; + // requires java.base; } diff --git a/test/dorkboxTest/network/AeronPubSubTest.kt b/test/dorkboxTest/network/AeronPubSubTest.kt new file mode 100644 index 00000000..9defe5b5 --- /dev/null +++ b/test/dorkboxTest/network/AeronPubSubTest.kt @@ -0,0 +1,522 @@ +/* + * Copyright 2024 dorkbox, llc + * + * 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 dorkboxTest.network + +import dorkbox.collections.LockFreeArrayList +import dorkbox.network.Configuration +import dorkbox.network.aeron.AeronDriver +import dorkbox.network.aeron.endpoint +import dorkbox.network.exceptions.ClientTimedOutException +import dorkbox.util.NamedThreadFactory +import io.aeron.CommonContext +import io.aeron.Publication +import kotlinx.atomicfu.atomic +import org.junit.Assert +import org.junit.Test +import org.slf4j.LoggerFactory +import java.util.concurrent.* + +class AeronPubSubTest : BaseTest() { + @Test + fun connectTestNormalPub() { + val log = LoggerFactory.getLogger("ConnectTest") + + // NOTE: once a config is assigned to a driver, the config cannot be changed + val totalCount = 40 + val port = 3535 + val serverStreamId = 55555 + val handshakeTimeoutNs = TimeUnit.SECONDS.toNanos(10) + + + + val serverDriver = run { + val conf = serverConfig() + conf.enableIPv6 = false + conf.uniqueAeronDirectory = true + + val driver = AeronDriver(conf, log, null) + driver.start() + driver + } + + val clientDrivers = mutableListOf() + val clientPublications = mutableListOf>() + + + for (i in 1..totalCount) { + val conf = clientConfig() + conf.enableIPv6 = false + conf.uniqueAeronDirectory = true + + val driver = AeronDriver(conf, log, null) + driver.start() + + clientDrivers.add(driver) + } + + + + val subscriptionUri = AeronDriver.uriHandshake(CommonContext.UDP_MEDIA, true) + .endpoint(true, "127.0.0.1", port) + val sub = serverDriver.addSubscription(subscriptionUri, serverStreamId, "server", false) + + var sessionID = 1234567 + clientDrivers.forEachIndexed { index, clientDriver -> + val publicationUri = AeronDriver.uri(CommonContext.UDP_MEDIA, sessionID++, true) + .endpoint(true, "127.0.0.1", port) + + // can throw an exception! We catch it in the calling class + val publication = clientDriver.addPublication(publicationUri, serverStreamId, "client_$index", false) + + // can throw an exception! We catch it in the calling class + // we actually have to wait for it to connect before we continue + clientDriver.waitForConnection(atomic(false), publication, handshakeTimeoutNs, "client_$index") { cause -> + ClientTimedOutException("Client publication cannot connect with localhost server", cause) + } + + clientPublications.add(Pair(clientDriver, publication)) + } + + + clientPublications.forEachIndexed { index, (clientDriver, pub) -> + clientDriver.close(pub, "client_$index") + } + + serverDriver.close(sub, "server") + + + clientDrivers.forEach { clientDriver -> + clientDriver.close() + } + + clientDrivers.forEach { clientDriver -> + clientDriver.ensureStopped(10_000, 500) + } + + serverDriver.close() + serverDriver.ensureStopped(10_000, 500) + + // have to make sure that the aeron driver is CLOSED. + Assert.assertTrue("The aeron drivers are not fully closed!", AeronDriver.areAllInstancesClosed()) + } + + @Test + fun connectTestExclusivePub() { + val log = LoggerFactory.getLogger("ConnectTest") + + // NOTE: once a config is assigned to a driver, the config cannot be changed + val totalCount = 40 + val port = 3535 + val serverStreamId = 55555 + val handshakeTimeoutNs = TimeUnit.SECONDS.toNanos(10) + + + + val serverDriver = run { + val conf = serverConfig() + conf.enableIPv6 = false + conf.uniqueAeronDirectory = true + + val driver = AeronDriver(conf, log, null) + driver.start() + driver + } + + val clientDrivers = mutableListOf() + val clientPublications = mutableListOf>() + + + for (i in 1..totalCount) { + val conf = clientConfig() + conf.enableIPv6 = false + conf.uniqueAeronDirectory = true + + val driver = AeronDriver(conf, log, null) + driver.start() + + clientDrivers.add(driver) + } + + + + val subscriptionUri = AeronDriver.uriHandshake(CommonContext.UDP_MEDIA, true) + .endpoint(true, "127.0.0.1", port) + val sub = serverDriver.addSubscription(subscriptionUri, serverStreamId, "server", false) + + var sessionID = 1234567 + clientDrivers.forEachIndexed { index, clientDriver -> + val publicationUri = AeronDriver.uri(CommonContext.UDP_MEDIA, sessionID++, true) + .endpoint(true, "127.0.0.1", port) + + // can throw an exception! We catch it in the calling class + val publication = clientDriver.addExclusivePublication(publicationUri, serverStreamId, "client_$index", false) + + // can throw an exception! We catch it in the calling class + // we actually have to wait for it to connect before we continue + clientDriver.waitForConnection(atomic(false), publication, handshakeTimeoutNs, "client_$index") { cause -> + ClientTimedOutException("Client publication cannot connect with localhost server", cause) + } + + clientPublications.add(Pair(clientDriver, publication)) + } + + + clientPublications.forEachIndexed { index, (clientDriver, pub) -> + clientDriver.close(pub, "client_$index") + } + + serverDriver.close(sub, "server") + + + clientDrivers.forEach { clientDriver -> + clientDriver.close() + } + + clientDrivers.forEach { clientDriver -> + clientDriver.ensureStopped(10_000, 500) + } + + serverDriver.close() + serverDriver.ensureStopped(10_000, 500) + + // have to make sure that the aeron driver is CLOSED. + Assert.assertTrue("The aeron drivers are not fully closed!", AeronDriver.areAllInstancesClosed()) + } + + @Test + fun reconnectTest() { + val log = LoggerFactory.getLogger("ConnectTest") + + // NOTE: once a config is assigned to a driver, the config cannot be changed + val totalCount = 40 + val port = 3535 + val serverStreamId = 55555 + val handshakeTimeoutNs = TimeUnit.SECONDS.toNanos(10) + + + + val serverDriver = run { + val conf = serverConfig() + conf.enableIPv6 = false + conf.uniqueAeronDirectory = true + + val driver = AeronDriver(conf, log, null) + driver.start() + driver + } + + val clientDrivers = mutableListOf() + val clientPublications = mutableListOf>() + + + for (i in 1..totalCount) { + val conf = clientConfig() + conf.enableIPv6 = false + conf.uniqueAeronDirectory = true + + val driver = AeronDriver(conf, log, null) + driver.start() + + clientDrivers.add(driver) + } + + + + val subscriptionUri = AeronDriver.uriHandshake(CommonContext.UDP_MEDIA, true) + .endpoint(true, "127.0.0.1", port) + val sub = serverDriver.addSubscription(subscriptionUri, serverStreamId, "server", false) + + var sessionID = 1234567 + clientDrivers.forEachIndexed { index, clientDriver -> + val publicationUri = AeronDriver.uri(CommonContext.UDP_MEDIA, sessionID++, true) + .endpoint(true, "127.0.0.1", port) + + // can throw an exception! We catch it in the calling class + val publication = clientDriver.addPublication(publicationUri, serverStreamId, "client_$index", false) + + // can throw an exception! We catch it in the calling class + // we actually have to wait for it to connect before we continue + clientDriver.waitForConnection(atomic(false), publication, handshakeTimeoutNs, "client_$index") { cause -> + ClientTimedOutException("Client publication cannot connect with localhost server", cause) + } + + clientPublications.add(Pair(clientDriver, publication)) + } + + + clientPublications.forEachIndexed { index, (clientDriver, pub) -> + clientDriver.close(pub, "client_$index") + } + + println("reconnecting..") + + // THE RECONNECT PART + clientDrivers.forEachIndexed { index, clientDriver -> + val publicationUri = AeronDriver.uri(CommonContext.UDP_MEDIA, sessionID++, true) + .endpoint(true, "127.0.0.1", port) + + // can throw an exception! We catch it in the calling class + val publication = clientDriver.addPublication(publicationUri, serverStreamId, "client_$index", false) + + // can throw an exception! We catch it in the calling class + // we actually have to wait for it to connect before we continue + clientDriver.waitForConnection(atomic(false), publication, handshakeTimeoutNs, "client_$index") { cause -> + ClientTimedOutException("Client publication cannot connect with localhost server", cause) + } + + clientPublications.add(Pair(clientDriver, publication)) + } + + clientPublications.forEachIndexed { index, (clientDriver, pub) -> + clientDriver.close(pub, "client_$index") + } + + + println("Closing..") + + clientDrivers.forEach { clientDriver -> + clientDriver.close() + } + + serverDriver.close(sub, "server") + + clientDrivers.forEach { clientDriver -> + clientDriver.ensureStopped(10_000, 500) + } + + serverDriver.close() + serverDriver.ensureStopped(10_000, 500) + + // have to make sure that the aeron driver is CLOSED. + Assert.assertTrue("The aeron drivers are not fully closed!", AeronDriver.areAllInstancesClosed()) + } + + @Test + fun reconnectMultiTest() { + val log = LoggerFactory.getLogger("ConnectTest") + + // NOTE: once a config is assigned to a driver, the config cannot be changed + val totalCount = 80 + val port = 3535 + val serverStreamId = 55555 + val handshakeTimeoutNs = TimeUnit.SECONDS.toNanos(10) + + + + val serverDriver = run { + val conf = serverConfig() + conf.enableIPv6 = false + conf.uniqueAeronDirectory = true + + val driver = AeronDriver(conf, log, null) + driver.start() + driver + } + + val clientDrivers = mutableListOf() + val clientPublications = LockFreeArrayList>() + + + for (i in 1..totalCount) { + val conf = clientConfig() + conf.enableIPv6 = false + conf.uniqueAeronDirectory = true + + val driver = AeronDriver(conf, log, null) + driver.start() + + clientDrivers.add(driver) + } + + + + val subscriptionUri = AeronDriver.uriHandshake(CommonContext.UDP_MEDIA, true) + .endpoint(true, "127.0.0.1", port) + val sub = serverDriver.addSubscription(subscriptionUri, serverStreamId, "server", false) + + val sessionID = atomic(1234567) + + + // if we are on the same JVM, the defaultScope for coroutines is SHARED, and limited! + val differentThreadLaunchers = Executors.newFixedThreadPool(totalCount/2, + NamedThreadFactory("Unit Test Client", Configuration.networkThreadGroup, true) + ) + + var latch = CountDownLatch(clientDrivers.size) + + clientDrivers.forEachIndexed { index, clientDriver -> + differentThreadLaunchers.submit { + val publicationUri = AeronDriver.uri(CommonContext.UDP_MEDIA, sessionID.getAndIncrement(), true).endpoint(true, "127.0.0.1", port) + + // can throw an exception! We catch it in the calling class + val publication = clientDriver.addPublication(publicationUri, serverStreamId, "client_$index", false) + + // can throw an exception! We catch it in the calling class + // we actually have to wait for it to connect before we continue + clientDriver.waitForConnection(atomic(false), publication, handshakeTimeoutNs, "client_$index") { cause -> + ClientTimedOutException("Client publication cannot connect with localhost server", cause) + } + + clientPublications.add(Pair(clientDriver, publication)) + latch.countDown() + } + } + + latch.await() + + clientPublications.forEachIndexed { index, (clientDriver, pub) -> + clientDriver.close(pub, "client_$index") + } + + println("reconnecting..") + latch = CountDownLatch(clientDrivers.size) + + // THE RECONNECT PART + clientDrivers.forEachIndexed { index, clientDriver -> + differentThreadLaunchers.submit { + val publicationUri = AeronDriver.uri(CommonContext.UDP_MEDIA, sessionID.getAndIncrement(), true).endpoint(true, "127.0.0.1", port) + + // can throw an exception! We catch it in the calling class + val publication = clientDriver.addPublication(publicationUri, serverStreamId, "client_$index", false) + + // can throw an exception! We catch it in the calling class + // we actually have to wait for it to connect before we continue + clientDriver.waitForConnection(atomic(false), publication, handshakeTimeoutNs, "client_$index") { cause -> + ClientTimedOutException("Client publication cannot connect with localhost server", cause) + } + + clientPublications.add(Pair(clientDriver, publication)) + latch.countDown() + } + } + latch.await() + + + clientPublications.forEachIndexed { index, (clientDriver, pub) -> + clientDriver.close(pub, "client_$index") + } + + + println("Closing..") + + clientDrivers.forEach { clientDriver -> + clientDriver.close() + } + + serverDriver.close(sub, "server") + + clientDrivers.forEach { clientDriver -> + clientDriver.ensureStopped(10_000, 500) + } + + serverDriver.close() + serverDriver.ensureStopped(10_000, 500) + + // have to make sure that the aeron driver is CLOSED. + Assert.assertTrue("The aeron drivers are not fully closed!", AeronDriver.areAllInstancesClosed()) + } + + + + @Test() + fun connectFailWithBadSessionIdTest() { + val log = LoggerFactory.getLogger("ConnectTest") + + // NOTE: once a config is assigned to a driver, the config cannot be changed + val totalCount = 40 + val port = 3535 + val serverStreamId = 55555 + val handshakeTimeoutNs = TimeUnit.SECONDS.toNanos(10) + + + + val serverDriver = run { + val conf = serverConfig() + conf.enableIPv6 = false + conf.uniqueAeronDirectory = true + + val driver = AeronDriver(conf, log, null) + driver.start() + driver + } + + val clientDrivers = mutableListOf() + val clientPublications = mutableListOf>() + + + for (i in 1..totalCount) { + val conf = clientConfig() + conf.enableIPv6 = false + conf.uniqueAeronDirectory = true + + val driver = AeronDriver(conf, log, null) + driver.start() + + clientDrivers.add(driver) + } + + + + val subscriptionUri = AeronDriver.uriHandshake(CommonContext.UDP_MEDIA, true) + .endpoint(true, "127.0.0.1", port) + val sub = serverDriver.addSubscription(subscriptionUri, serverStreamId, "server", false) + + try { + var sessionID = 1234567 + clientDrivers.forEachIndexed { index, clientDriver -> + val publicationUri = AeronDriver.uri(CommonContext.UDP_MEDIA, sessionID, true) + .endpoint(true, "127.0.0.1", port) + + + // can throw an exception! We catch it in the calling class + val publication = clientDriver.addPublication(publicationUri, serverStreamId, "client_$index", false) + + // can throw an exception! We catch it in the calling class + // we actually have to wait for it to connect before we continue + clientDriver.waitForConnection(atomic(false), publication, handshakeTimeoutNs, "client_$index") { cause -> + ClientTimedOutException("Client publication cannot connect with localhost server", cause) + } + + clientPublications.add(Pair(clientDriver, publication)) + } + Assert.fail("TimeoutException should be caught!") + } catch (ignore: Exception) { + } + + + clientPublications.forEachIndexed { index, (clientDriver, pub) -> + clientDriver.close(pub, "client_$index") + } + + serverDriver.close(sub, "server") + + + clientDrivers.forEach { clientDriver -> + clientDriver.close() + } + + clientDrivers.forEach { clientDriver -> + clientDriver.ensureStopped(10_000, 500) + } + + serverDriver.close() + serverDriver.ensureStopped(10_000, 500) + + // have to make sure that the aeron driver is CLOSED. + Assert.assertTrue("The aeron drivers are not fully closed!", AeronDriver.areAllInstancesClosed()) + } +} diff --git a/test/dorkboxTest/network/BaseTest.kt b/test/dorkboxTest/network/BaseTest.kt index c83eb0f2..ad89aad1 100644 --- a/test/dorkboxTest/network/BaseTest.kt +++ b/test/dorkboxTest/network/BaseTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,25 +12,6 @@ * 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. - * - * Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package dorkboxTest.network @@ -40,34 +21,34 @@ import ch.qos.logback.classic.encoder.PatternLayoutEncoder import ch.qos.logback.classic.joran.JoranConfigurator import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.ConsoleAppender -import dorkbox.network.Client -import dorkbox.network.ClientConfiguration -import dorkbox.network.Configuration -import dorkbox.network.Server -import dorkbox.network.ServerConfiguration +import dorkbox.hex.toHexString +import dorkbox.network.* +import dorkbox.network.aeron.AeronDriver +import dorkbox.network.connection.Connection import dorkbox.network.connection.EndPoint import dorkbox.os.OS import dorkbox.storage.Storage import dorkbox.util.entropy.Entropy import dorkbox.util.entropy.SimpleEntropy import dorkbox.util.exceptions.InitializationException -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.DelicateCoroutinesApi import org.junit.After import org.junit.Assert import org.junit.Before import org.slf4j.LoggerFactory -import java.io.File -import java.lang.Thread.sleep import java.lang.reflect.Field import java.lang.reflect.Method import java.util.concurrent.* +@OptIn(DelicateCoroutinesApi::class) abstract class BaseTest { companion object { const val LOCALHOST = "localhost" + const val DEBUG = false + // wait minimum of 3 minutes before we automatically fail the unit test. - var AUTO_FAIL_TIMEOUT: Long = 180L + var AUTO_FAIL_TIMEOUT: Long = if (DEBUG) 9999999999L else 180L init { if (OS.javaVersion >= 9) { @@ -96,7 +77,7 @@ abstract class BaseTest { // } // } - setLogLevel(Level.TRACE) +// setLogLevel(Level.TRACE) // setLogLevel(Level.ERROR) // setLogLevel(Level.DEBUG) @@ -108,13 +89,19 @@ abstract class BaseTest { } } + fun pause(timeToSleep: Long) { + Thread.sleep(timeToSleep) + } + fun clientConfig(block: Configuration.() -> Unit = {}): ClientConfiguration { val configuration = ClientConfiguration() + configuration.appId = "network_test" + configuration.tag = "**Client**" configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk! - configuration.port = 2200 configuration.enableIpc = false + configuration.enableIPv6 = false block(configuration) return configuration @@ -122,10 +109,11 @@ abstract class BaseTest { fun serverConfig(block: ServerConfiguration.() -> Unit = {}): ServerConfiguration { val configuration = ServerConfiguration() + configuration.appId = "network_test" configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk! - configuration.port = 2200 configuration.enableIpc = false + configuration.enableIPv6 = false configuration.maxClientCount = 50 configuration.maxConnectionsPerIpAddress = 50 @@ -140,146 +128,240 @@ abstract class BaseTest { // assume SLF4J is bound to logback in the current environment val rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger rootLogger.detachAndStopAllAppenders() - rootLogger.level = level val context = rootLogger.loggerContext val jc = JoranConfigurator() jc.context = context - jc.doConfigure(File("logback.xml").absoluteFile) +// jc.doConfigure(File("logback.xml").absoluteFile) context.reset() // override default configuration - - // we only want error messages - val nettyLogger = LoggerFactory.getLogger("io.netty") as Logger - nettyLogger.level = Level.ERROR - - // we only want error messages - val kryoLogger = LoggerFactory.getLogger("com.esotericsoftware") as Logger - kryoLogger.level = Level.ERROR - - val encoder = PatternLayoutEncoder() encoder.context = context - encoder.pattern = "%date{HH:mm:ss.SSS} %-5level [%logger{35}] %msg%n" + encoder.pattern = "%date{HH:mm:ss.SSS} %-5level [%logger{35}] [%t] %msg%n" encoder.start() + val consoleAppender = ConsoleAppender() consoleAppender.context = context consoleAppender.encoder = encoder consoleAppender.start() + + rootLogger.addAppender(consoleAppender) + + // modify the level AFTER we setup the context! + + rootLogger.level = level + + // we only want error messages + val nettyLogger = LoggerFactory.getLogger("io.netty") as Logger + nettyLogger.level = Level.ERROR + + // we only want error messages + val kryoLogger = LoggerFactory.getLogger("com.esotericsoftware") as Logger + kryoLogger.level = Level.ERROR } } - @Volatile - private var latch = CountDownLatch(1) - @Volatile private var autoFailThread: Thread? = null private val endPointConnections: MutableList> = CopyOnWriteArrayList() + private var errors = mutableListOf() + @Volatile private var isStopping = false + private val logger: org.slf4j.Logger = LoggerFactory.getLogger(this.javaClass.simpleName)!! + init { - println("---- " + this.javaClass.simpleName) + setLogLevel(Level.TRACE) - // we must always make sure that aeron is shut-down before starting again. - while (Server.isRunning(serverConfig())) { - println("Aeron was still running. Waiting for it to stop...") - sleep(2000) + if (DEBUG) { + logger.error("---- " + this.javaClass.simpleName + " :: DEBUG UNIT TESTS ENABLED") + } else { + logger.error("---- " + this.javaClass.simpleName) } + + AeronDriver.checkForMemoryLeaks() } - fun addEndPoint(endPointConnection: EndPoint<*>) { - endPointConnections.add(endPointConnection) - latch = CountDownLatch(endPointConnections.size + 1) + + fun addEndPoint(endPoint: EndPoint<*>, runCheck: Boolean = true) { + if (runCheck && !endPoint.ensureStopped()) { + throw IllegalStateException("Unable to continue, AERON was unable to stop.") + } + + endPoint.onInit { logger.error("UNIT TEST: init $id (${uuid.toHexString()})") } + endPoint.onConnect { logger.error("UNIT TEST: connect $id (${uuid.toHexString()})") } + endPoint.onDisconnect { logger.error("UNIT TEST: disconnect $id (${uuid.toHexString()})") } + + endPoint.onError { + logger.error("UNIT TEST: ERROR! $id (${uuid.toHexString()})", it) + errors.add(it) + } + + endPoint.onErrorGlobal { + logger.error("UNIT TEST: GLOBAL ERROR!", it) + errors.add(it) + } + + endPointConnections.add(endPoint) } /** - * Immediately stop the endpoints + * Immediately stop the endpoints. DOES NOT WAIT FOR THEM TO CLOSE! + * + * Can stop from inside different callbacks + * - message (network event poller) + * - connect (eventdispatch.connect) + * - disconnect (eventdispatch.connect) */ fun stopEndPoints(stopAfterMillis: Long = 0L) { if (isStopping) { return } + +// if (EventDispatcher.isCurrentEvent()) { +// val mutex = Mutex(true) +// +// // we want to redispatch, in the event we are already running inside the event dispatch +// // this gives us the chance to properly exit/close WITHOUT blocking currentEventDispatch +// // during the `waitForClose()` call +// GlobalScope.launch { +// stopEndPoints(stopAfterMillis) +// mutex.unlock() +// } +// +// runBlocking { +// mutex.withLock { } +// } +// +// return +// } + isStopping = true - // not the best, but this works for our purposes. This is a TAD hacky, because we ALSO have to make sure that we - // ARE NOT in the same thread group as netty! if (stopAfterMillis > 0L) { - sleep(stopAfterMillis) + Thread.sleep(stopAfterMillis) } - // we start with "1", so make sure adjust if we want an accurate count - println("Shutting down ${endPointConnections.size} (${latch.count - 1}) endpoints...") + val clients = endPointConnections.filterIsInstance>() + val servers = endPointConnections.filterIsInstance>() - val remainingConnections = mutableListOf>() + logger.error("Unit test shutting down ${clients.size} clients...") + logger.error("Unit test shutting down ${servers.size} server...") // shutdown clients first - endPointConnections.forEach { endPoint -> - if (endPoint is Client) { - endPoint.close() - latch.countDown() - println("Done closing: ${endPoint.type.simpleName}") - } else { - remainingConnections.add(endPoint) - } + logger.error("Closing clients...") + clients.forEach { endPoint -> + endPoint.close() } + logger.error("NOT WAITING FOR CLIENT CLOSE.") + // shutdown everything else (should only be servers) last - println("Shutting down ${remainingConnections.size} (${latch.count - 1}) endpoints...") - remainingConnections.forEach { + logger.error("Closing servers...") + servers.forEach { it.close() - latch.countDown() } + logger.error("NOT WAITING FOR SERVER CLOSE.") - // we start with "1", so make sure to end it - latch.countDown() - endPointConnections.clear() + logger.error("Closed endpoints...") } /** - * Wait for network client/server threads to shutdown for the specified time. 0 will wait forever + * Wait for network client/server threads to shut down for the specified time. 0 will wait forever * * it should close as close to naturally as possible, otherwise there are problems * * @param stopAfterSeconds how many seconds to wait, the default is 2 minutes. */ - fun waitForThreads(stopAfterSeconds: Long = AUTO_FAIL_TIMEOUT, preShutdownAction: () -> Unit = {}) { - val latchTriggered = try { - if (stopAfterSeconds == 0L) { - latch.await(Long.MAX_VALUE, TimeUnit.SECONDS) - } else { - latch.await(stopAfterSeconds, TimeUnit.SECONDS) - } - } catch (e: InterruptedException) { - e.printStackTrace() - false + fun waitForThreads(stopAfterSeconds: Long = AUTO_FAIL_TIMEOUT, onShutdown: (List) -> List = { it }) { + val clients = endPointConnections.filterIsInstance>() + val servers = endPointConnections.filterIsInstance>() + + val timeoutMS = TimeUnit.SECONDS.toMillis(stopAfterSeconds) + var successClients = true + var successServers = true + + clients.forEach { endPoint -> + successClients = successClients && endPoint.waitForClose(timeoutMS) + } + servers.forEach { endPoint -> + successServers = successServers && endPoint.waitForClose(timeoutMS) + } + + clients.forEach { endPoint -> + endPoint.stopDriver() + } + servers.forEach { endPoint -> + endPoint.stopDriver() } // run actions before we actually shutdown, but after we wait - if (!latchTriggered) { - preShutdownAction() + if (!successClients || !successServers) { + Assert.fail("Shutdown latch not triggered ($successClients|$successServers)!") } - // always stop the endpoints - stopEndPoints() + // we must always make sure that aeron is shut-down before starting again. + clients.forEach { endPoint -> + endPoint.ensureStopped() + endPoint.shutdownEventDispatcher() // once shutdown, it cannot be restarted! + + if (!Client.ensureStopped(endPoint.config.copy())) { + throw IllegalStateException("Unable to continue, AERON client was unable to stop.") + } + } + + servers.forEach { endPoint -> + endPoint.ensureStopped() + endPoint.shutdownEventDispatcher() // once shutdown, it cannot be restarted! + + if (!Client.ensureStopped(endPoint.config.copy())) { + throw IllegalStateException("Unable to continue, AERON server was unable to stop.") + } + } + + if (!AeronDriver.areAllInstancesClosed(logger)) { + throw RuntimeException("Unable to shutdown! There are still Aeron drivers loaded!") + } + + logger.error("UNIT TEST, checking driver and memory leaks") + + // have to make sure that the aeron driver is CLOSED. + Assert.assertTrue("The aeron drivers are not fully closed!", AeronDriver.areAllInstancesClosed()) + AeronDriver.checkForMemoryLeaks() + + endPointConnections.clear() + + logger.error("Finished shutting down all endpoints... ($successClients, $successServers)") + + if (errors.isNotEmpty()) { + val acceptableErrors = errors.filterNot { it.message?.contains("Unable to send message. (Connection in non-connected state, aborted attempt! Not connected)") ?: false } + val filteredErrors = onShutdown(acceptableErrors) + if (filteredErrors.isNotEmpty()) { + filteredErrors.forEach { + it.printStackTrace() + } + + Assert.fail("Exception caught, and it shouldn't have happened!") + } + } } @Before fun setupFailureCheck() { - autoFailThread = Thread(Runnable { + autoFailThread = Thread({ // not the best, but this works for our purposes. This is a TAD hacky, because we ALSO have to make sure that we // ARE NOT in the same thread group as netty! try { Thread.sleep(AUTO_FAIL_TIMEOUT * 1000L) // if the thread is interrupted, then it means we finished the test. - System.err.println("Test did not complete in a timely manner...") - runBlocking { - stopEndPoints(0L) - } + LoggerFactory.getLogger(this.javaClass.simpleName).error("Test did not complete in a timely manner...") + stopEndPoints() + waitForThreads() Assert.fail("Test did not complete in a timely manner.") } catch (ignored: InterruptedException) { } diff --git a/test/dorkboxTest/network/ConnectionFilterTest.kt b/test/dorkboxTest/network/ConnectionFilterTest.kt index 6161fed0..95f57755 100644 --- a/test/dorkboxTest/network/ConnectionFilterTest.kt +++ b/test/dorkboxTest/network/ConnectionFilterTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network import dorkbox.netUtil.IPv4 @@ -11,26 +27,27 @@ import kotlinx.atomicfu.atomic import org.junit.Assert import org.junit.Test +@Suppress("UNUSED_ANONYMOUS_PARAMETER") class ConnectionFilterTest : BaseTest() { @Test fun autoAcceptAll() { val serverConnectSuccess = atomic(false) val clientConnectSuccess = atomic(false) - run { + val server = run { val configuration = serverConfig() val server: Server = Server(configuration) addEndPoint(server) - server.bind() server.onConnect { serverConnectSuccess.value = true close() } + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) @@ -44,14 +61,17 @@ class ConnectionFilterTest : BaseTest() { stopEndPoints() } - try { - client.connect(LOCALHOST) - } catch (e: Exception) { - stopEndPoints() - throw e - } + client } + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + waitForThreads() + throw e + } waitForThreads() @@ -64,22 +84,21 @@ class ConnectionFilterTest : BaseTest() { val serverConnectSuccess = atomic(false) val clientConnectSuccess = atomic(false) - run { + val server = run { val configuration = serverConfig() val server: Server = Server(configuration) addEndPoint(server) - server.bind() server.filter(IpSubnetFilterRule(IPv4.WILDCARD, 0)) server.filter(IpSubnetFilterRule(IPv6.WILDCARD, 0)) - server.onConnect { serverConnectSuccess.value = true close() } + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) @@ -93,12 +112,16 @@ class ConnectionFilterTest : BaseTest() { stopEndPoints() } - try { - client.connect(LOCALHOST) - } catch (e: Exception) { - stopEndPoints() - throw e - } + client + } + + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + waitForThreads() + throw e } waitForThreads() @@ -112,12 +135,12 @@ class ConnectionFilterTest : BaseTest() { val serverConnectSuccess = atomic(false) val clientConnectSuccess = atomic(false) - run { + val server = run { val configuration = serverConfig() val server: Server = Server(configuration) addEndPoint(server) - server.bind() + server.filter(IpSubnetFilterRule("1.1.1.1", 0)) server.filter(IpSubnetFilterRule("::1.1.1.1", 0)) // compressed ipv6 @@ -125,9 +148,10 @@ class ConnectionFilterTest : BaseTest() { serverConnectSuccess.value = true close() } + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) @@ -141,12 +165,16 @@ class ConnectionFilterTest : BaseTest() { stopEndPoints() } - try { - client.connect(LOCALHOST) - } catch (e: Exception) { - stopEndPoints() - throw e - } + client + } + + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + waitForThreads() + throw e } waitForThreads() @@ -160,43 +188,46 @@ class ConnectionFilterTest : BaseTest() { val serverConnectSuccess = atomic(false) val clientConnectSuccess = atomic(false) - run { + val server = run { val configuration = serverConfig() val server: Server = Server(configuration) addEndPoint(server) - server.bind() + server.filter(IpSubnetFilterRule(IPv4.WILDCARD, 0)) + server.filter(IpSubnetFilterRule(IPv6.WILDCARD, 0)) server.onConnect { serverConnectSuccess.value = true - println("Closing server connection") close() } + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) addEndPoint(client) - client.filter(IpSubnetFilterRule(IPv4.WILDCARD, 0)) - client.filter(IpSubnetFilterRule(IPv6.WILDCARD, 0)) + client.onConnect { clientConnectSuccess.value = true } client.onDisconnect { - println("**************************** CLOSE") stopEndPoints() } - try { - client.connect(LOCALHOST) - } catch (e: Exception) { - stopEndPoints() - throw e - } + client + } + + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + waitForThreads() + throw e } waitForThreads() @@ -209,39 +240,48 @@ class ConnectionFilterTest : BaseTest() { @Test(expected = ClientException::class) fun rejectServer() { - run { - val configuration = serverConfig() - - val server: Server = Server(configuration) + val server = run { + val serverConfig = serverConfig() + val server: Server = Server(serverConfig) addEndPoint(server) - server.bind() server.filter(IpSubnetFilterRule("1.1.1.1", 32)) // this address will NEVER actually connect. we just use it for testing server.onConnect { close() } + + server } - run { - val config = clientConfig() - val client: Client = Client(config) + val client = run { + val clientConfig = clientConfig() + + val client: Client = Client(clientConfig) addEndPoint(client) client.onDisconnect { stopEndPoints() } - try { - client.connect(LOCALHOST) - } catch (e: Exception) { - e.printStackTrace() - stopEndPoints() - throw e - } + client } - waitForThreads() + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + waitForThreads { errors -> + errors.filterNot { + it.message?.contains("Connection was not permitted!") ?: false + } + } + throw e + } + + // fail, since we should have thrown an exception + Assert.assertFalse(true) } @Test @@ -251,23 +291,24 @@ class ConnectionFilterTest : BaseTest() { val serverConnectSuccess = atomic(false) val clientConnectSuccess = atomic(false) - run { + val server = run { val configuration = serverConfig() { enableIpc = true } val server: Server = Server(configuration) addEndPoint(server) - server.bind() server.filter(IpSubnetFilterRule("1.1.1.1", 32)) // this address will NEVER actually connect. we just use it for testing server.onConnect { - serverConnectSuccess.lazySet(true) + serverConnectSuccess.value = true close() } + + server } - run { + val client = run { val config = clientConfig() { enableIpc = true } @@ -280,16 +321,19 @@ class ConnectionFilterTest : BaseTest() { } client.onDisconnect { + logger.error("STARTING TO CLOSE CLIENT") stopEndPoints() } - try { - client.connect(LOCALHOST) - } catch (e: Exception) { - e.printStackTrace() - stopEndPoints() - // this is expected. - } + client + } + + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + // this is expected. } waitForThreads() @@ -300,35 +344,43 @@ class ConnectionFilterTest : BaseTest() { @Test(expected = ClientException::class) fun rejectClient() { - run { + val server = run { val configuration = serverConfig() val server: Server = Server(configuration) addEndPoint(server) - server.bind() - + server.filter(IpSubnetFilterRule("1.1.1.1", 32)) // this address will NEVER actually connect. we just use it for testing server.onConnect { close() } + + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) addEndPoint(client) - client.filter(IpSubnetFilterRule("1.1.1.1", 32)) // this address will NEVER actually connect. we just use it for testing client.onDisconnect { stopEndPoints() } - try { - client.connect(LOCALHOST) - } catch (e: Exception) { - stopEndPoints() - throw e - } + client + } + + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + waitForThreads { errors -> + errors.filterNot { + it.message?.contains("Connection was not permitted!") ?: false + } + } + throw e } waitForThreads() @@ -339,13 +391,12 @@ class ConnectionFilterTest : BaseTest() { val serverConnectSuccess = atomic(false) val clientConnectSuccess = atomic(false) - run { + val server = run { val configuration = serverConfig() val server: Server = Server(configuration) addEndPoint(server) - server.bind() - server.filter { + server.filter { clientAddress, tagName -> true } @@ -353,9 +404,11 @@ class ConnectionFilterTest : BaseTest() { serverConnectSuccess.value = true close() } + + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) @@ -369,14 +422,17 @@ class ConnectionFilterTest : BaseTest() { stopEndPoints() } - try { - client.connect(LOCALHOST) - } catch (e: Exception) { - stopEndPoints() - throw e - } + client } + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + waitForThreads() + throw e + } waitForThreads() @@ -389,46 +445,44 @@ class ConnectionFilterTest : BaseTest() { val serverConnectSuccess = atomic(false) val clientConnectSuccess = atomic(false) - run { + val server = run { val configuration = serverConfig() val server: Server = Server(configuration) addEndPoint(server) - server.bind() - server.onConnect { serverConnectSuccess.value = true - logger.error { "closing" } close() } + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) addEndPoint(client) - client.filter { - true - } + client.onConnect { clientConnectSuccess.value = true } client.onDisconnect { - logger.error { "on close" } stopEndPoints() } - try { - client.connect(LOCALHOST) - } catch (e: Exception) { - stopEndPoints() - throw e - } + client } + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + waitForThreads() + throw e + } waitForThreads() @@ -439,22 +493,23 @@ class ConnectionFilterTest : BaseTest() { @Test(expected = ClientException::class) fun rejectCustomServer() { - run { + val server = run { val configuration = serverConfig() val server: Server = Server(configuration) addEndPoint(server) - server.bind() - server.filter { + + server.filter { clientAddress, tagName -> false } server.onConnect { close() } + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) @@ -464,12 +519,20 @@ class ConnectionFilterTest : BaseTest() { stopEndPoints() } - try { - client.connect(LOCALHOST) - } catch (e: Exception) { - stopEndPoints() - throw e - } + client + } + + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + waitForThreads{ errors -> + errors.filterNot { + it.message?.contains("Connection was not permitted!") ?: false + } + } + throw e } waitForThreads() @@ -477,39 +540,104 @@ class ConnectionFilterTest : BaseTest() { @Test(expected = ClientException::class) fun rejectCustomClient() { - run { + val server = run { val configuration = serverConfig() val server: Server = Server(configuration) addEndPoint(server) - server.bind() + server.filter { clientAddress, tagName -> + false + } server.onConnect { close() } + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) addEndPoint(client) - client.filter { - false - } client.onDisconnect { stopEndPoints() } - try { - client.connect(LOCALHOST) - } catch (e: Exception) { + client + } + + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + waitForThreads{ errors -> + errors.filterNot { + it.message?.contains("Connection was not permitted!") ?: false + } + } + + throw e + } + + waitForThreads() + } + + + @Test + fun acceptAllCustomClientNoPendingMessages() { + val serverConnectSuccess = atomic(false) + val clientConnectSuccess = atomic(false) + + val server = run { + val configuration = serverConfig() + + val server: Server = Server(configuration) + addEndPoint(server) + + server.enableBufferedMessages { clientAddress, tagName -> + false + } + + server.onConnect { + serverConnectSuccess.value = true + close() + } + server + } + + val client = run { + val config = clientConfig() + + val client: Client = Client(config) + addEndPoint(client) + + + client.onConnect { + clientConnectSuccess.value = true + } + + client.onDisconnect { stopEndPoints() - throw e } + + client + } + + server.bind(2000) + try { + client.connect(LOCALHOST, 2000) + } catch (e: Exception) { + stopEndPoints() + waitForThreads() + throw e } waitForThreads() + + Assert.assertTrue(serverConnectSuccess.value) + Assert.assertTrue(clientConnectSuccess.value) } } diff --git a/test/dorkboxTest/network/DisconnectReconnectTest.kt b/test/dorkboxTest/network/DisconnectReconnectTest.kt index 68f31ad1..680782aa 100644 --- a/test/dorkboxTest/network/DisconnectReconnectTest.kt +++ b/test/dorkboxTest/network/DisconnectReconnectTest.kt @@ -1,139 +1,161 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network import dorkbox.network.Client import dorkbox.network.Server import dorkbox.network.aeron.AeronDriver import dorkbox.network.connection.Connection +import dorkbox.network.connection.EndPoint +import dorkbox.network.rmi.RemoteObject import kotlinx.atomicfu.atomic -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Test -import java.io.IOException +import org.slf4j.LoggerFactory +import java.util.concurrent.* class DisconnectReconnectTest : BaseTest() { - private val reconnectCount = atomic(0) + private val reconnects = 5 @Test fun reconnectClient() { - run { - val configuration = serverConfig() + val latch = CountDownLatch(reconnects+1) + val reconnectCount = atomic(0) + + val server = run { + val config = serverConfig() + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) - val server: Server = Server(configuration) + val server: Server = Server(config) addEndPoint(server) - server.bind() server.onConnect { logger.error("Disconnecting after 2 seconds.") - delay(2000) + pause(2000L) logger.error("Disconnecting....") close() } + + server } - run { + val client = run { val config = clientConfig() + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) val client: Client = Client(config) addEndPoint(client) - client.onDisconnect { + latch.countDown() logger.error("Disconnected!") val count = reconnectCount.getAndIncrement() - if (count == 3) { - logger.error("Shutting down") - stopEndPoints() - } - else { + if (count < reconnects) { logger.error("Reconnecting: $count") - try { - client.connect(LOCALHOST) - } catch (e: IOException) { - e.printStackTrace() - } + client.reconnect() } } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) + latch.await() + stopEndPoints() waitForThreads() System.err.println("Connection count (after reconnecting) is: " + reconnectCount.value) - Assert.assertEquals(4, reconnectCount.value) + Assert.assertEquals(reconnects+1, reconnectCount.value) } @Test fun reconnectClientViaClientClose() { - run { - val configuration = serverConfig { + val latch = CountDownLatch(reconnects+1) + val reconnectCount = atomic(0) + + val server = run { + val config = serverConfig { uniqueAeronDirectory = true } - val server: Server = Server(configuration) + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) + + val server: Server = Server(config) addEndPoint(server) - server.bind() + server } - run { - val config = clientConfig() { + val client = run { + val config = clientConfig { uniqueAeronDirectory = true } + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) + val client: Client = Client(config) addEndPoint(client) client.onConnect { logger.error("Disconnecting after 2 seconds.") - delay(2000) + pause(2000) logger.error("Disconnecting....") - client.close() + close() } client.onDisconnect { + latch.countDown() logger.error("Disconnected!") val count = reconnectCount.getAndIncrement() - if (count == 3) { - logger.error("Shutting down") - stopEndPoints() - } - else { + if (count < reconnects) { logger.error("Reconnecting: $count") - try { - client.connect(LOCALHOST) - } catch (e: IOException) { - e.printStackTrace() - } + client.reconnect() } } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) + latch.await() + stopEndPoints() waitForThreads() System.err.println("Connection count (after reconnecting) is: " + reconnectCount.value) - Assert.assertEquals(4, reconnectCount.value) + Assert.assertEquals(reconnects+1, reconnectCount.value) } interface CloseIface { - suspend fun close() + fun close() } class CloseImpl : CloseIface { - override suspend fun close() { + override fun close() { // the connection specific one is called instead } - suspend fun close(connection: Connection) { - connection.logger.error { "PRE CLOSE MESSAGE!" } + fun close(connection: Connection) { connection.close() } } @@ -141,31 +163,41 @@ class DisconnectReconnectTest : BaseTest() { @Test fun reconnectRmiClient() { + val latch = CountDownLatch(reconnects+1) + val reconnectCount = atomic(0) + val CLOSE_ID = 33 - run { + val server = run { val config = serverConfig() + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) config.serialization.rmi.register(CloseIface::class.java) val server: Server = Server(config) addEndPoint(server) - server.bind() - server.onConnect { logger.error("Disconnecting after 2 seconds.") - delay(2000) + pause(2000) logger.error("Disconnecting via RMI ....") val closerObject = rmi.get(CLOSE_ID) + + // the close operation will kill the connection, preventing the response from returning. + RemoteObject.cast(closerObject).async = true + + // this just calls connection.close() (on the client) closerObject.close() } + + server } - run { + val client = run { val config = clientConfig() config.serialization.rmi.register(CloseIface::class.java, CloseImpl::class.java) + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) val client: Client = Client(config) addEndPoint(client) @@ -175,117 +207,126 @@ class DisconnectReconnectTest : BaseTest() { } client.onDisconnect { + latch.countDown() logger.error("Disconnected!") val count = reconnectCount.getAndIncrement() - if (count == 3) { - logger.error("Shutting down") - stopEndPoints() - } - else { + if (count < reconnects) { logger.error("Reconnecting: $count") - try { - client.connect(LOCALHOST) - } catch (e: IOException) { - e.printStackTrace() - } + client.reconnect() } } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) - waitForThreads(AUTO_FAIL_TIMEOUT*10) + latch.await() + stopEndPoints() + waitForThreads() //System.err.println("Connection count (after reconnecting) is: " + reconnectCount.value) - Assert.assertEquals(4, reconnectCount.value) + Assert.assertEquals(reconnects+1, reconnectCount.value) } @Test fun manualMediaDriverAndReconnectClient() { + val latch = CountDownLatch(reconnects+1) + val reconnectCount = atomic(0) + + val log = LoggerFactory.getLogger("DCUnitTest") // NOTE: once a config is assigned to a driver, the config cannot be changed - val aeronDriver = AeronDriver(serverConfig()) - runBlocking { - aeronDriver.start() - } + val aeronDriver = AeronDriver(serverConfig(), log, null) + aeronDriver.start() - run { - val serverConfiguration = serverConfig() - val server: Server = Server(serverConfiguration) - addEndPoint(server) - server.bind() + val server = run { + val config = serverConfig() + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) + + val server: Server = Server(config) + addEndPoint(server, false) server.onConnect { logger.error("Disconnecting after 2 seconds.") - delay(2000) + pause(2000) logger.error("Disconnecting....") close() } + + server } - run { + val client = run { val config = clientConfig() + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) val client: Client = Client(config) - addEndPoint(client) + addEndPoint(client, false) client.onDisconnect { + latch.countDown() logger.error("Disconnected!") val count = reconnectCount.getAndIncrement() - if (count == 3) { - logger.error("Shutting down") - stopEndPoints() - } - else { + if (count < reconnects) { logger.error("Reconnecting: $count") - try { - client.connect(LOCALHOST) - } catch (e: IOException) { - e.printStackTrace() - } + client.reconnect() } } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) + latch.await() + stopEndPoints() waitForThreads() - runBlocking { - aeronDriver.close() - } + + aeronDriver.close() //System.err.println("Connection count (after reconnecting) is: " + reconnectCount.value) - Assert.assertEquals(4, reconnectCount.value) + Assert.assertEquals(reconnects+1, reconnectCount.value) } @Test fun reconnectWithFallbackClient() { + if (EndPoint.DEBUG_CONNECTIONS) { + throw RuntimeException("DEBUG_CONNECTIONS is enabled. This will cause the test to run forever!!") + } + + val latch = CountDownLatch(reconnects+1) + val reconnectCount = atomic(0) + // this tests IPC with fallback to UDP (because the server has IPC disabled, and the client has it enabled) - run { + val server = run { val config = serverConfig() config.enableIpc = false + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) val server: Server = Server(config) addEndPoint(server) - server.bind() server.onConnect { logger.error("Disconnecting after 2 seconds.") - delay(2000) + pause(2000) logger.error("Disconnecting....") close() } + + server } - run { + val client = run { val config = clientConfig() config.enableIpc = true + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) val client: Client = Client(config) addEndPoint(client) @@ -293,56 +334,53 @@ class DisconnectReconnectTest : BaseTest() { client.onDisconnect { logger.error("Disconnected!") + latch.countDown() val count = reconnectCount.getAndIncrement() - if (count == 3) { - logger.error("Shutting down") - stopEndPoints() - } - else { + if (count < reconnects) { logger.error("Reconnecting: $count") - try { - client.connect(LOCALHOST) - } catch (e: IOException) { - e.printStackTrace() - } + client.reconnect() } } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) + latch.await() + stopEndPoints() waitForThreads() //System.err.println("Connection count (after reconnecting) is: " + reconnectCount.value) - Assert.assertEquals(4, reconnectCount.value) + Assert.assertEquals(reconnects+1, reconnectCount.value) } @Test fun disconnectedMediaDriver() { - val server: Server - run { + val server = run { val config = serverConfig() config.enableIpc = false config.uniqueAeronDirectory = true + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) - server = Server(config) + val server = Server(config) addEndPoint(server) - server.bind() server.onConnect { logger.error("Connected!") } + server } - val client: Client - run { + val client = run { val config = clientConfig() config.enableIpc = false config.uniqueAeronDirectory = true + config.connectionCloseTimeoutInSeconds = 0 // we want the unit test to go fast (there will be a limit with aeron linger, etc) - client = Client(config) + val client = Client(config) addEndPoint(client) client.onConnect { @@ -353,11 +391,13 @@ class DisconnectReconnectTest : BaseTest() { stopEndPoints() } - client.connect(LOCALHOST) + client } - server.close() + server.bind(2000) + client.connect(LOCALHOST, 2000) + server.close() waitForThreads() } diff --git a/test/dorkboxTest/network/ErrorLoggerTest.kt b/test/dorkboxTest/network/ErrorLoggerTest.kt index 10b28684..160be4b7 100644 --- a/test/dorkboxTest/network/ErrorLoggerTest.kt +++ b/test/dorkboxTest/network/ErrorLoggerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,9 @@ class ErrorLoggerTest : BaseTest() { @Test fun customErrorLoggerTest() { - run { + val exception = Exception("server ERROR. SHOULD BE CAUGHT") + + val server = run { val configuration = serverConfig() configuration.aeronErrorFilter = { true // log all errors @@ -45,18 +47,18 @@ class ErrorLoggerTest : BaseTest() { } server.onErrorGlobal { throwable -> - println("Global error") + println("Global error!!!!") throwable.printStackTrace() } server.onMessage { - throw Exception("server ERROR. SHOULD BE CAUGHT") + throw exception } - server.bind() + server } - run { + val client = run { val config = clientConfig() config.aeronErrorFilter = { true // log all errors @@ -68,12 +70,20 @@ class ErrorLoggerTest : BaseTest() { client.onConnect { // can be any message, we just want the error-log to log something send(TestObj()) + + pause(200) stopEndPoints() } - client.connect(LOCALHOST) + client } - waitForThreads() + server.bind(2000) + client.connect(LOCALHOST, 2000) + + waitForThreads() { errors -> + // we don't want to fail the unit test for this exception + errors.filter { it != exception } + } } } diff --git a/test/dorkboxTest/network/ListenerTest.kt b/test/dorkboxTest/network/ListenerTest.kt index 8035e0c3..774fa4ba 100644 --- a/test/dorkboxTest/network/ListenerTest.kt +++ b/test/dorkboxTest/network/ListenerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,7 +12,8 @@ * 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. - * + */ + /* * Copyright (c) 2008, Nathan Sweet * All rights reserved. * @@ -66,18 +67,22 @@ class ListenerTest : BaseTest() { // quick and dirty test to also test connection sub-classing internal open inner class TestConnectionA(connectionParameters: ConnectionParams) : Connection(connectionParameters) { open fun check() { - overrideCheck.value = true + overrideCheck.lazySet(true) } } @Test @Throws(SecurityException::class, InitializationException::class, IOException::class, InterruptedException::class) fun listener() { - val server: Server = Server(serverConfig()) { - TestConnectionA(it) + val server = object : Server(serverConfig()) { + override fun newConnection(connectionParameters: ConnectionParams): TestConnectionA { + return TestConnectionA(connectionParameters) + } } + addEndPoint(server) + // has session/stream count errors! // standard listener server.onMessage { message -> logger.error ("server string message") @@ -89,14 +94,14 @@ class ListenerTest : BaseTest() { // generic listener server.onMessage { // should be called! - serverOnMessage.value = true + serverOnMessage.lazySet(true) logger.error ("server any message") } // standard connect check server.onConnect { logger.error ("server connect") - serverConnect.value = true + serverConnect.lazySet(true) onMessage { logger.error ("server connection any message") @@ -112,37 +117,38 @@ class ListenerTest : BaseTest() { // standard listener disconnect check server.onDisconnect { logger.error ("server disconnect") - serverDisconnect.value = true + serverDisconnect.lazySet(true) } - server.bind() - // ---- - val client: Client = Client(clientConfig()) { - TestConnectionA(it) + val client = object : Client(clientConfig()) { + override fun newConnection(connectionParameters: ConnectionParams): TestConnectionA { + return TestConnectionA(connectionParameters) + } } + addEndPoint(client) client.onConnect { - logger.error { "client connect 1" } + logger.error("client connect 1") send(origString) // 20 a's } // standard connect check client.onConnect { - logger.error { "client connect 2" } - clientConnect.value = true + logger.error("client connect 2") + clientConnect.lazySet(true) } client.onMessage { message -> - logger.error { "client string message" } + logger.error("client string message") if (origString != message) { - checkFail2.value = true + checkFail2.lazySet(true) System.err.println("original string not equal to the string received") stopEndPoints() return@onMessage @@ -158,11 +164,12 @@ class ListenerTest : BaseTest() { // standard listener disconnect check client.onDisconnect { logger.error ("client disconnect") - clientDisconnect.value = true + clientDisconnect.lazySet(true) } - client.connect(LOCALHOST) + server.bind(2000) + client.connect(LOCALHOST, 2000) waitForThreads() diff --git a/test/dorkboxTest/network/MultiClientTest.kt b/test/dorkboxTest/network/MultiClientTest.kt index 1635c5a6..57c5d8f7 100644 --- a/test/dorkboxTest/network/MultiClientTest.kt +++ b/test/dorkboxTest/network/MultiClientTest.kt @@ -1,97 +1,119 @@ +/* + * Copyright 2024 dorkbox, llc + * + * 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 dorkboxTest.network -import ch.qos.logback.classic.Level import dorkbox.network.Client +import dorkbox.network.Configuration import dorkbox.network.Server import dorkbox.network.connection.Connection +import dorkbox.util.NamedThreadFactory +import io.aeron.driver.ThreadingMode import kotlinx.atomicfu.atomic -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assert import org.junit.Test import java.text.SimpleDateFormat import java.util.* +import java.util.concurrent.* +@Suppress("UNUSED_ANONYMOUS_PARAMETER") class MultiClientTest : BaseTest() { - private val totalCount = 20 private val clientConnectCount = atomic(0) private val serverConnectCount = atomic(0) private val disconnectCount = atomic(0) + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) @Test fun multiConnectClient() { - setLogLevel(Level.TRACE) + // this can be upped to 100 for stress testing, but for general unit tests this should be smaller (as this is sensitive on the load of the machine) + // THE ONLY limitation you will have with this, is the size of the temp drive space. + val totalCount = 30 + + + val server = run { + val config = serverConfig() + config.uniqueAeronDirectory = true + config.threadingMode = ThreadingMode.DEDICATED + + val server: Server = Server(config) + addEndPoint(server) + server.onConnect { + val count = serverConnectCount.incrementAndGet() + + logger.error("${this.id} - Connected $count ....") + close() + } + server + } + + + println() + println() + println() + println() + + val shutdownLatch = CountDownLatch(totalCount) // clients first, so they try to connect to the server at (roughly) the same time val clients = mutableListOf>() for (i in 1..totalCount) { val config = clientConfig() - config.enableIPv6 = false config.uniqueAeronDirectory = true - val client: Client = Client(config, "Client$i") - client.onConnect { - val count = clientConnectCount.getAndIncrement() - - logger.error("${this.id} - Connected $count ($i)!") + val client: Client = Client(config, "Client $i") + client.onInit { + val count = clientConnectCount.incrementAndGet() + logger.error("$id - Connected $count ($i)!") } + client.onDisconnect { - disconnectCount.getAndIncrement() - logger.error("${this.id} - Disconnected $i!") + val count = disconnectCount.incrementAndGet() + logger.error("$id - Disconnected $count ($i)!") + shutdownLatch.countDown() } + addEndPoint(client) clients += client } - // start up the drivers first - runBlocking { - clients.forEach { - it.startDriver() - } - } - - val configuration = serverConfig() - configuration.enableIPv6 = false - - val server: Server = Server(configuration) - addEndPoint(server) - server.onConnect { - val count = serverConnectCount.incrementAndGet() - - logger.error("${this.id} - Connecting $count ....") - close() - if (count == totalCount) { - logger.error { "Stopping endpoints!" } - delay(6000) - outputStats(server) + server.bind(2000, 2001) - delay(2000) - outputStats(server) - - delay(2000) - outputStats(server) - - stopEndPoints(10000L) - } + // start up the drivers first + clients.forEach { + it.startDriver() } - server.bind() + // if we are on the same JVM, the defaultScope for coroutines is SHARED, and limited! + val differentThreadLaunchers = Executors.newFixedThreadPool(totalCount/2, + NamedThreadFactory("Unit Test Client", Configuration.networkThreadGroup, true) + ) - GlobalScope.launch { - clients.forEach { - // long connection timeout, since the more that try to connect at the same time, the longer it takes to setup aeron (since it's all shared) - launch { it.connect(LOCALHOST, 300*totalCount) } + clients.forEachIndexed { count, client -> + differentThreadLaunchers.submit { + client.connect(LOCALHOST, 2000, 2001, 30) } } - waitForThreads() { - outputStats(server) - } - + shutdownLatch.await() + stopEndPoints() + waitForThreads() Assert.assertEquals(totalCount, clientConnectCount.value) Assert.assertEquals(totalCount, serverConnectCount.value) diff --git a/test/dorkboxTest/network/MultipleServerTest.kt b/test/dorkboxTest/network/MultipleServerTest.kt index 4808677a..2fa32313 100644 --- a/test/dorkboxTest/network/MultipleServerTest.kt +++ b/test/dorkboxTest/network/MultipleServerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,7 +12,8 @@ * 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. - * + */ +/* * Copyright (c) 2008, Nathan Sweet * All rights reserved. * @@ -35,10 +36,13 @@ package dorkboxTest.network import dorkbox.network.Client +import dorkbox.network.Configuration import dorkbox.network.Server import dorkbox.network.connection.Connection +import dorkbox.network.exceptions.ServerException import dorkbox.util.exceptions.SecurityException import org.junit.Assert +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import java.io.File @@ -46,24 +50,22 @@ import java.io.IOException import java.util.concurrent.atomic.* class MultipleServerTest : BaseTest() { - val total = 5 - var received = AtomicInteger() + private val total = 4 + @Test @Throws(SecurityException::class, IOException::class) fun multipleUDP() { - val portOffset = 2 - received.set(0) + val received = AtomicInteger(0) var serverAeronDir: File? = null val didReceive = mutableListOf() + val servers = mutableListOf>() for (count in 0 until total) { didReceive.add(AtomicBoolean()) - val offset = count * portOffset val configuration = serverConfig() - configuration.port += offset configuration.aeronDirectory = serverAeronDir configuration.enableIpc = false @@ -82,20 +84,18 @@ class MultipleServerTest : BaseTest() { } } - server.bind() + servers.add(server) serverAeronDir = File(configuration.aeronDirectory.toString() + count) } var clientAeronDir: File? = null val didSend = mutableListOf() + val clients = mutableListOf>() for (count in 0 until total) { didSend.add(AtomicBoolean()) - val offset = count * portOffset - val configuration = clientConfig() - configuration.port += offset configuration.aeronDirectory = clientAeronDir configuration.enableIpc = false @@ -110,7 +110,15 @@ class MultipleServerTest : BaseTest() { send("client_$count") } - client.connect(LOCALHOST) + clients.add(client) + } + + for (count in 0 until total) { + servers[count].bind(2000 + (count*2)) + } + + for (count in 0 until total) { + clients[count].connect(LOCALHOST, 2000 + (count*2)) } waitForThreads() @@ -123,31 +131,66 @@ class MultipleServerTest : BaseTest() { } } + @Test(expected = ServerException::class) + @Throws(SecurityException::class, IOException::class) + fun multipleInvalidIPC() { + val servers = mutableListOf>() + try { + for (count in 0 until total) { + val configuration = serverConfig() + configuration.enableIPv4 = true + configuration.enableIPv6 = true + configuration.enableIpc = true + + servers.add(Server(configuration, "server_$count")) + } + } catch (e: Exception) { + servers.forEach { + it.close() + it.waitForClose() + } + throw e + } + } + @Test @Throws(SecurityException::class, IOException::class) fun multipleIPC() { - val portOffset = 2 - received.set(0) + val received = AtomicInteger(0) + + // client and server must share locations + val aeronDirs = mutableListOf() + for (count in 0 until total) { + val baseFileLocation = Configuration.defaultAeronLogLocation() + aeronDirs.add(File(baseFileLocation, "aeron_${count}")) + } + - var serverAeronDir: File? = null val didReceive = mutableListOf() + val servers = mutableListOf>() for (count in 0 until total) { didReceive.add(AtomicBoolean()) - val offset = count * portOffset val configuration = serverConfig() - configuration.port += offset - configuration.aeronDirectory = serverAeronDir + configuration.aeronDirectory = aeronDirs[count] + configuration.enableIPv4 = false + configuration.enableIPv6 = false configuration.enableIpc = true - val server: Server = Server(configuration) + val server: Server = Server(configuration, "server_$count") addEndPoint(server) - server.onMessage{ message -> - if (message != "client_$count") { - Assert.fail() - } + server.onInit { + logger.warn("INIT: $count") + } + + server.onConnect { + logger.warn("CONNECT: $count") + } + + server.onMessage { message -> + assertEquals(message, "client_$count") didReceive[count].set(true) if (received.incrementAndGet() == total) { @@ -156,34 +199,44 @@ class MultipleServerTest : BaseTest() { } } - server.bind() - - serverAeronDir = File(configuration.aeronDirectory.toString() + count) + servers.add(server) } - var clientAeronDir: File? = null val didSend = mutableListOf() + val clients = mutableListOf>() for (count in 0 until total) { didSend.add(AtomicBoolean()) - val offset = count * portOffset val configuration = clientConfig() - configuration.port += offset - configuration.aeronDirectory = clientAeronDir + configuration.aeronDirectory = aeronDirs[count] + configuration.enableIPv4 = false + configuration.enableIPv6 = false configuration.enableIpc = true - val client: Client = Client(configuration) + val client: Client = Client(configuration, "client_$count") addEndPoint(client) - clientAeronDir = File(configuration.aeronDirectory.toString() + count) + client.onInit { + logger.warn("INIT: $count") + } client.onConnect { + logger.warn("CONNECT: $count") didSend[count].set(true) send("client_$count") } - client.connect(LOCALHOST) + clients.add(client) + } + + for (count in 0 until total) { + servers[count].bindIpc() + } + + + for (count in 0 until total) { + clients[count].connectIpc() } waitForThreads() diff --git a/test/dorkboxTest/network/PingPongTest.kt b/test/dorkboxTest/network/PingPongTest.kt index 14491304..6efd7bfc 100644 --- a/test/dorkboxTest/network/PingPongTest.kt +++ b/test/dorkboxTest/network/PingPongTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,7 +12,8 @@ * 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. - * + */ +/* * Copyright (c) 2008, Nathan Sweet * All rights reserved. * @@ -32,6 +33,7 @@ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + package dorkboxTest.network import dorkbox.network.Client @@ -43,42 +45,39 @@ import org.junit.Test import java.util.concurrent.atomic.* class PingPongTest : BaseTest() { - @Volatile - private var fail: String? = null var tries = 1000 @Test fun pingPong() { - fail = "Data not received." val data = Data() populateData(data) - run { + val server = run { val config = serverConfig() register(config.serialization) val server: Server = Server(config) addEndPoint(server) - server.bind() server.onError { throwable -> - fail = "Error during processing. $throwable" + logger.error("Error during processing", throwable) + stopEndPoints() + Assert.fail("Error during processing") } server.onConnect { - server.forEachConnection { connection -> - connection.logger.error("server connection: $connection") - } + this.logger.error("server connection: $this") } server.onMessage { message -> send(message) } + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) @@ -87,19 +86,20 @@ class PingPongTest : BaseTest() { client.onConnect { logger.error("client connection: $this") - - fail = null send(data) } client.onError { throwable -> - fail = "Error during processing. $throwable" - throwable.printStackTrace() + logger.error("Error during processing", throwable) + stopEndPoints() + Assert.fail("Error during processing") } val counter = AtomicInteger(0) client.onMessage { _ -> - if (counter.getAndIncrement() <= tries) { + val count = counter.getAndIncrement() + if (count <= tries) { + logger.error("Ran: $count") send(data) } else { logger.error("done.") @@ -108,16 +108,13 @@ class PingPongTest : BaseTest() { } } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) - waitForThreads() - - - if (fail != null) { - Assert.fail(fail) - } + waitForThreads(3000) } private fun register(manager: Serialization<*>) { diff --git a/test/dorkboxTest/network/PingTest.kt b/test/dorkboxTest/network/PingTest.kt index 70f073a8..777750d9 100644 --- a/test/dorkboxTest/network/PingTest.kt +++ b/test/dorkboxTest/network/PingTest.kt @@ -1,6 +1,21 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network -import ch.qos.logback.classic.Level import dorkbox.network.Client import dorkbox.network.Server import dorkbox.network.connection.Connection @@ -12,19 +27,18 @@ class PingTest : BaseTest() { val counter = atomic(0) @Test fun RmiPing() { - setLogLevel(Level.TRACE) - + // session/stream count errors val clientSuccess = atomic(false) - run { + val server = run { val configuration = serverConfig() val server: Server = Server(configuration) addEndPoint(server) - server.bind() + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) @@ -35,8 +49,6 @@ class PingTest : BaseTest() { ping { // a ping object is returned, once the round-trip is complete val count = counter.getAndIncrement() - println(count) - if (count == 99) { clientSuccess.value = true @@ -50,10 +62,13 @@ class PingTest : BaseTest() { } } - client.connect(LOCALHOST) + client } - waitForThreads(500) + server.bind(2000) + client.connect(LOCALHOST, 2000) + + waitForThreads() Assert.assertTrue(clientSuccess.value) } diff --git a/test/dorkboxTest/network/RoundTripMessageTest.kt b/test/dorkboxTest/network/RoundTripMessageTest.kt index 571a1549..c4f68847 100644 --- a/test/dorkboxTest/network/RoundTripMessageTest.kt +++ b/test/dorkboxTest/network/RoundTripMessageTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network import dorkbox.network.Client @@ -12,16 +28,16 @@ import org.junit.Test class RoundTripMessageTest : BaseTest() { @Test fun MessagePing() { + // session/stream count errors val serverSuccess = atomic(false) val clientSuccess = atomic(false) - run { + val server = run { val configuration = serverConfig() configuration.serialization.register(PingMessage::class.java) val server: Server = Server(configuration) addEndPoint(server) - server.bind() server.onMessage { ping -> serverSuccess.value = true @@ -29,9 +45,11 @@ class RoundTripMessageTest : BaseTest() { ping.pongTime = System.currentTimeMillis() send(ping) } + + server } - run { + val client = run { val config = clientConfig() val client: Client = Client(config) @@ -59,9 +77,12 @@ class RoundTripMessageTest : BaseTest() { send(ping) } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) + waitForThreads() Assert.assertTrue(serverSuccess.value) diff --git a/test/dorkboxTest/network/SendSyncTest.kt b/test/dorkboxTest/network/SendSyncTest.kt new file mode 100644 index 00000000..d60f2fde --- /dev/null +++ b/test/dorkboxTest/network/SendSyncTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network + +import dorkbox.network.Client +import dorkbox.network.Server +import dorkbox.network.connection.Connection +import kotlinx.atomicfu.atomic +import org.junit.Assert +import org.junit.Test + +class SendSyncTest : BaseTest() { + val counter = atomic(0) + + @Test + fun sendSync() { + // session/stream count errors + val serverSuccess = atomic(false) + val clientSuccess = atomic(false) + + val server = run { + val configuration = serverConfig() + + val server: Server = Server(configuration) + addEndPoint(server) + + server.onMessage { + serverSuccess.value = true + } + + server + } + + val client = run { + val config = clientConfig() + + val client: Client = Client(config) + addEndPoint(client) + + client.onConnect { + repeat(100) { + send("Hi, I'm waiting!") { + // a send-sync object is returned, once the round-trip is complete, and we are notified + val count = counter.getAndIncrement() + if (count == 99) { + clientSuccess.value = true + + stopEndPoints() + } + } + } + } + + client + } + + server.bind(2000) + client.connect(LOCALHOST, 2000) + + waitForThreads() + + Assert.assertTrue(clientSuccess.value) + Assert.assertTrue(serverSuccess.value) + } +} diff --git a/test/dorkboxTest/network/SerializationValidationTest.kt b/test/dorkboxTest/network/SerializationValidationTest.kt index 6391bf7a..f6bcd2b7 100644 --- a/test/dorkboxTest/network/SerializationValidationTest.kt +++ b/test/dorkboxTest/network/SerializationValidationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,16 @@ package dorkboxTest.network import dorkbox.network.Client import dorkbox.network.Server import dorkbox.network.connection.Connection -import dorkbox.network.serialization.KryoExtra import dorkbox.network.serialization.Serialization +import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Test class SerializationValidationTest : BaseTest() { @Test fun checkManyObjects() { - run { + // session/stream count errors + val server = run { val configuration = serverConfig() register(configuration.serialization) @@ -36,11 +37,11 @@ class SerializationValidationTest : BaseTest() { server.onMessage { _ -> stopEndPoints() } - server.bind() + server } - run { + val client = run { val configuration = clientConfig() val client = Client(configuration) @@ -50,31 +51,19 @@ class SerializationValidationTest : BaseTest() { send(FinishedCommand()) } - client.connect(LOCALHOST) + client } - waitForThreads() - } - - @Test - fun checkTakeKryo() { - @Suppress("UNCHECKED_CAST") - val serialization = serverConfig().serialization as Serialization - - val kryos = mutableListOf>() - for (i in 0 until 17) { - kryos.add(serialization.takeKryo()) - } + server.bind(2000) + client.connect(LOCALHOST, 2000) - kryos.forEach { - serialization.returnKryo(it) - } + waitForThreads() } - @Test fun checkOutOfOrder() { - run { + // session/stream count errors + val server = run { val configuration = serverConfig() configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) configuration.serialization.register(TestObjectImpl::class.java) // this is again, on purpose to verify registration order! @@ -87,11 +76,11 @@ class SerializationValidationTest : BaseTest() { server.onMessage { _ -> stopEndPoints() } - server.bind() + server } - run { + val client = run { val configuration = clientConfig() val client = Client(configuration) @@ -101,7 +90,9 @@ class SerializationValidationTest : BaseTest() { logger.error("Connected") rmi.getGlobal(1).apply { logger.error("Starting test") - setValue(43.21f) + runBlocking { + setValue(43.21f) + } // Normal remote method call. Assert.assertEquals(43.21f, other(), .0001f) @@ -112,15 +103,18 @@ class SerializationValidationTest : BaseTest() { } } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) + waitForThreads() } @Test fun checkOutOfOrder2() { - run { + val server = run { val configuration = serverConfig() configuration.serialization.rmi.register(TestObject::class.java) configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) @@ -133,11 +127,11 @@ class SerializationValidationTest : BaseTest() { server.onMessage { _ -> stopEndPoints() } - server.bind() + server } - run { + val client = run { val configuration = clientConfig() val client = Client(configuration) @@ -147,7 +141,9 @@ class SerializationValidationTest : BaseTest() { logger.error("Connected") rmi.getGlobal(1).apply { logger.error("Starting test") - setValue(43.21f) + runBlocking { + setValue(43.21f) + } // Normal remote method call. Assert.assertEquals(43.21f, other(), .0001f) @@ -158,9 +154,12 @@ class SerializationValidationTest : BaseTest() { } } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) + waitForThreads() } diff --git a/test/dorkboxTest/network/ShutdownWhileInBadStateTest.kt b/test/dorkboxTest/network/ShutdownWhileInBadStateTest.kt new file mode 100644 index 00000000..2ca3b419 --- /dev/null +++ b/test/dorkboxTest/network/ShutdownWhileInBadStateTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network + +import dorkbox.network.Client +import dorkbox.network.Server +import dorkbox.network.connection.Connection +import dorkbox.network.connection.ConnectionParams +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.junit.Test + +class ShutdownWhileInBadStateTest : BaseTest() { + + @Test + fun shutdownWhileClientConnecting() { + val isRunning = atomic(true) + + val client = run { + val config = clientConfig() + + val client = object : Client(config) { + override fun newConnection(connectionParameters: ConnectionParams): Connection { + return Connection(connectionParameters) + } + } + + addEndPoint(client) + + client + } + + GlobalScope.launch(Dispatchers.IO) { + // there might be errors while trying to reconnect. Make sure that we constantly retry. + while (isRunning.value) { + try { + // if the server isn't available, wait forever. + client.connect(LOCALHOST, 2222, connectionTimeoutSec = 0) // bad port + return@launch + } catch (ignored: Exception) { + } + } + } + + // at wait for the connection process to start + Thread.sleep(4000) + isRunning.lazySet(false) + client.close() + + waitForThreads() + } + + @Test + fun shutdownServerBeforeBind() { + val server = run { + val configuration = serverConfig() + + val server: Server = Server(configuration) + addEndPoint(server) + + server + } + + server.close() + + waitForThreads() + } +} diff --git a/test/dorkboxTest/network/SimplePortTest.kt b/test/dorkboxTest/network/SimplePortTest.kt new file mode 100644 index 00000000..2945e3b4 --- /dev/null +++ b/test/dorkboxTest/network/SimplePortTest.kt @@ -0,0 +1,317 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network + +import dorkbox.netUtil.IPv4 +import dorkbox.netUtil.IPv6 +import dorkbox.network.Client +import dorkbox.network.Server +import dorkbox.network.connection.Connection +import dorkbox.util.exceptions.SecurityException +import org.junit.Assert +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.IOException +import java.util.concurrent.atomic.* + +class SimplePortTest : BaseTest() { + private var received = AtomicBoolean() + private val sent = AtomicBoolean() + + enum class ConnectType(val ip4: Boolean, val ip6: Boolean, val ipc: Boolean) { + IPC(false, false, true), + IPC4(true, false, true), + IPC6(false, true, true), + IPC46(true, true, true), + IPC64(true, true, true), + IP4(true, false, false), + IP6(false, true, false), + IP46(true, true, false), + IP64(true, true, false) + } + + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp4Server() { + simpleServerShutdown(ConnectType.IP4) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp6Server() { + simpleServerShutdown(ConnectType.IP6) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp46Server() { + simpleServerShutdown(ConnectType.IP46) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp64Server() { + simpleServerShutdown(ConnectType.IP64) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpcServer() { + simpleServerShutdown(ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc4FallbackServer() { + simpleServerShutdown(ConnectType.IPC4, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc6FallbackServer() { + simpleServerShutdown(ConnectType.IPC6, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc46FallbackServer() { + simpleServerShutdown(ConnectType.IPC46, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc64FallbackServer() { + simpleServerShutdown(ConnectType.IPC64, ConnectType.IPC) + } + + + //////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////// + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp4Client() { + simpleClientShutdown(ConnectType.IP4) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp6Client() { + simpleClientShutdown(ConnectType.IP6) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp46Client() { + simpleClientShutdown(ConnectType.IP46) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp64Client() { + simpleClientShutdown(ConnectType.IP64) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpcClient() { + simpleClientShutdown(ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc4FallbackClient() { + simpleClientShutdown(ConnectType.IPC4, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc6FallbackClient() { + simpleClientShutdown(ConnectType.IPC6, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc46FallbackClient() { + simpleClientShutdown(ConnectType.IPC46, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc64FallbackClient() { + simpleClientShutdown(ConnectType.IPC64, ConnectType.IPC) + } + + // shutdown from the server + private fun simpleServerShutdown(clientType: ConnectType, serverType: ConnectType = clientType) { + received.set(false) + sent.set(false) + + val server = run { + val configuration = serverConfig() + + configuration.enableIPv4 = serverType.ip4 + configuration.enableIPv6 = serverType.ip6 + configuration.enableIpc = serverType.ipc + + val server: Server = Server(configuration) + addEndPoint(server) + + server.onMessage { message -> + if (message != "client") { + stopEndPoints() + Assert.fail("Wrong message!") + } + + if (this.isNetwork && this.remotePort != 2400) { + stopEndPoints() + Assert.fail("Wrong port: ${this.remotePort}!!") + } + + received.set(true) + logger.error("Done, stopping endpoints") + + // this must NOT be on the disconnect thread, because we cancel it! + stopEndPoints() + } + + server + } + + val client = run { + val configuration = clientConfig() + + configuration.port = 2400 // this is the port the client will receive return traffic on + configuration.enableIPv4 = clientType.ip4 + configuration.enableIPv6 = clientType.ip6 + configuration.enableIpc = clientType.ipc + + + val client: Client = Client(configuration) + addEndPoint(client) + + client.onConnect { + sent.set(true) + send("client") + } + + client + } + + val port1 = 1200 + val port2 = 1201 + + server.bind(port1, port2) + + when (clientType) { + ConnectType.IPC -> { client.connectIpc() } + ConnectType.IPC4 -> { client.connect(IPv4.LOCALHOST, port1, port2) } + ConnectType.IPC6 -> { client.connect(IPv6.LOCALHOST, port1, port2) } + ConnectType.IPC46 -> { client.connect(IPv4.LOCALHOST, port1, port2) } + ConnectType.IPC64 -> { client.connect(IPv6.LOCALHOST, port1, port2) } + ConnectType.IP4 -> { client.connect(IPv4.LOCALHOST, port1, port2) } + ConnectType.IP6 -> { client.connect(IPv6.LOCALHOST, port1, port2) } + ConnectType.IP46 -> { client.connect(IPv4.LOCALHOST, port1, port2) } + ConnectType.IP64 -> { client.connect(IPv6.LOCALHOST, port1, port2) } + } + + + waitForThreads() + + assertTrue(sent.get()) + assertTrue(received.get()) + } + + // shutdown from the client + private fun simpleClientShutdown(clientType: ConnectType, serverType: ConnectType = clientType) { + received.set(false) + sent.set(false) + + val server = run { + val configuration = serverConfig() + + configuration.enableIPv4 = serverType.ip4 + configuration.enableIPv6 = serverType.ip6 + configuration.enableIpc = serverType.ipc + + val server: Server = Server(configuration) + addEndPoint(server) + + server.onMessage { message -> + if (message != "client") { + stopEndPoints() + Assert.fail("Wrong message!") + } + + if (this.isNetwork && this.remotePort != 2400) { + stopEndPoints() + Assert.fail("Wrong port!") + } + + received.set(true) + logger.error("Done, stopping endpoints") + close() + } + + server + } + + val client = run { + val configuration = clientConfig() + + configuration.port = 2400 // this is the port the client will receive return traffic on + configuration.enableIPv4 = clientType.ip4 + configuration.enableIPv6 = clientType.ip6 + configuration.enableIpc = clientType.ipc + + + val client: Client = Client(configuration) + addEndPoint(client) + + client.onConnect { + sent.set(true) + send("client") + } + + client.onDisconnect { + stopEndPoints() + } + + client + } + + server.bind(12312) + when (clientType) { + ConnectType.IPC -> { client.connectIpc() } + ConnectType.IPC4 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IPC6 -> { client.connect(IPv6.LOCALHOST, 12312) } + ConnectType.IPC46 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IPC64 -> { client.connect(IPv6.LOCALHOST, 12312) } + ConnectType.IP4 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IP6 -> { client.connect(IPv6.LOCALHOST, 12312) } + ConnectType.IP46 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IP64 -> { client.connect(IPv6.LOCALHOST, 12312) } + } + + waitForThreads() + + assertTrue(sent.get()) + assertTrue(received.get()) + } +} diff --git a/test/dorkboxTest/network/SimpleTest.kt b/test/dorkboxTest/network/SimpleTest.kt new file mode 100644 index 00000000..cb991195 --- /dev/null +++ b/test/dorkboxTest/network/SimpleTest.kt @@ -0,0 +1,297 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network + +import dorkbox.netUtil.IPv4 +import dorkbox.netUtil.IPv6 +import dorkbox.network.Client +import dorkbox.network.Server +import dorkbox.network.connection.Connection +import dorkbox.util.exceptions.SecurityException +import org.junit.Assert +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.IOException +import java.util.concurrent.atomic.* + +class SimpleTest : BaseTest() { + private var received = AtomicBoolean() + private val sent = AtomicBoolean() + + enum class ConnectType(val ip4: Boolean, val ip6: Boolean, val ipc: Boolean) { + IPC(false, false, true), + IPC4(true, false, true), + IPC6(false, true, true), + IPC46(true, true, true), + IPC64(true, true, true), + IP4(true, false, false), + IP6(false, true, false), + IP46(true, true, false), + IP64(true, true, false) + } + + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp4Server() { + simpleServerShutdown(ConnectType.IP4) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp6Server() { + simpleServerShutdown(ConnectType.IP6) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp46Server() { + simpleServerShutdown(ConnectType.IP46) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp64Server() { + simpleServerShutdown(ConnectType.IP64) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpcServer() { + simpleServerShutdown(ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc4FallbackServer() { + simpleServerShutdown(ConnectType.IPC4, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc6FallbackServer() { + simpleServerShutdown(ConnectType.IPC6, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc46FallbackServer() { + simpleServerShutdown(ConnectType.IPC46, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc64FallbackServer() { + simpleServerShutdown(ConnectType.IPC64, ConnectType.IPC) + } + + + //////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////// + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp4Client() { + simpleClientShutdown(ConnectType.IP4) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp6Client() { + simpleClientShutdown(ConnectType.IP6) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp46Client() { + simpleClientShutdown(ConnectType.IP46) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIp64Client() { + simpleClientShutdown(ConnectType.IP64) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpcClient() { + simpleClientShutdown(ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc4FallbackClient() { + simpleClientShutdown(ConnectType.IPC4, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc6FallbackClient() { + simpleClientShutdown(ConnectType.IPC6, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc46FallbackClient() { + simpleClientShutdown(ConnectType.IPC46, ConnectType.IPC) + } + + @Test + @Throws(SecurityException::class, IOException::class) + fun simpleIpc64FallbackClient() { + simpleClientShutdown(ConnectType.IPC64, ConnectType.IPC) + } + + // shutdown from the server + private fun simpleServerShutdown(clientType: ConnectType, serverType: ConnectType = clientType) { + received.set(false) + sent.set(false) + + val server = run { + val configuration = serverConfig() + + configuration.enableIPv4 = serverType.ip4 + configuration.enableIPv6 = serverType.ip6 + configuration.enableIpc = serverType.ipc + + val server: Server = Server(configuration) + addEndPoint(server) + + server.onMessage { message -> + if (message != "client") { + Assert.fail() + } + + received.set(true) + logger.error("Done, stopping endpoints") + + stopEndPoints() + } + + server + } + + val client = run { + val configuration = clientConfig() + + configuration.enableIPv4 = clientType.ip4 + configuration.enableIPv6 = clientType.ip6 + configuration.enableIpc = clientType.ipc + + + val client: Client = Client(configuration) + addEndPoint(client) + + client.onConnect { + sent.set(true) + send("client") + } + client + } + + + server.bind(12312) + when (clientType) { + ConnectType.IPC -> { client.connectIpc() } + ConnectType.IPC4 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IPC6 -> { client.connect(IPv6.LOCALHOST, 12312) } + ConnectType.IPC46 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IPC64 -> { client.connect(IPv6.LOCALHOST, 12312) } + ConnectType.IP4 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IP6 -> { client.connect(IPv6.LOCALHOST, 12312) } + ConnectType.IP46 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IP64 -> { client.connect(IPv6.LOCALHOST, 12312) } + } + + waitForThreads() + + assertTrue(sent.get()) + assertTrue(received.get()) + } + + // shutdown from the client + private fun simpleClientShutdown(clientType: ConnectType, serverType: ConnectType = clientType) { + received.set(false) + sent.set(false) + + val server = run { + val configuration = serverConfig() + + configuration.enableIPv4 = serverType.ip4 + configuration.enableIPv6 = serverType.ip6 + configuration.enableIpc = serverType.ipc + + val server: Server = Server(configuration) + addEndPoint(server) + + server.onMessage { message -> + if (message != "client") { + Assert.fail() + } + + received.set(true) + logger.error("Done, stopping endpoints") + close() + } + + server + } + + val client = run { + val configuration = clientConfig() + + configuration.enableIPv4 = clientType.ip4 + configuration.enableIPv6 = clientType.ip6 + configuration.enableIpc = clientType.ipc + + + val client: Client = Client(configuration) + addEndPoint(client) + + client.onConnect { + sent.set(true) + send("client") + } + + client.onDisconnect { + stopEndPoints() + } + + client + } + + server.bind(12312) + when (clientType) { + ConnectType.IPC -> { client.connectIpc() } + ConnectType.IPC4 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IPC6 -> { client.connect(IPv6.LOCALHOST, 12312) } + ConnectType.IPC46 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IPC64 -> { client.connect(IPv6.LOCALHOST, 12312) } + ConnectType.IP4 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IP6 -> { client.connect(IPv6.LOCALHOST, 12312) } + ConnectType.IP46 -> { client.connect(IPv4.LOCALHOST, 12312) } + ConnectType.IP64 -> { client.connect(IPv6.LOCALHOST, 12312) } + } + + waitForThreads() + + assertTrue(sent.get()) + assertTrue(received.get()) + } +} diff --git a/test/dorkboxTest/network/StorageTest.kt b/test/dorkboxTest/network/StorageTest.kt index e64e0565..aa61d187 100644 --- a/test/dorkboxTest/network/StorageTest.kt +++ b/test/dorkboxTest/network/StorageTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,9 @@ import dorkbox.network.Server import dorkbox.network.connection.Connection import dorkbox.network.serialization.SettingsStore import dorkbox.storage.Storage -import mu.KotlinLogging import org.junit.Assert import org.junit.Test +import org.slf4j.LoggerFactory import java.io.File class StorageTest : BaseTest() { @@ -34,28 +34,23 @@ class StorageTest : BaseTest() { val serverConfig = serverConfig { settingsStore = sharedStore } - val server = Server(serverConfig) - server.bind() - val config = clientConfig { settingsStore = sharedStore } - val client = Client(config) - - client.connect(LOCALHOST) - Assert.assertTrue(server.storage.getSalt().contentEquals(client.storage.getSalt())) + val serverSalt = Server(serverConfig).use { it.storage.salt } + val clientSalt = Client(config).use { it.storage.salt } - server.close() + Assert.assertTrue(serverSalt.contentEquals(clientSalt)) } @Test fun memoryTest() { - val salt1 = SettingsStore(Storage.Memory(), KotlinLogging.logger("test1")).use { it.getSalt() } + val salt1 = SettingsStore(Storage.Memory(), LoggerFactory.getLogger("test1")).use { it.salt } - val salt2 = Server(serverConfig().apply { settingsStore = Storage.Memory() }).use { it.storage.getSalt() } - val salt3 = Server(serverConfig().apply { settingsStore = Storage.Memory() }).use { it.storage.getSalt() } + val salt2 = Server(serverConfig().apply { settingsStore = Storage.Memory() }).use { it.storage.salt } + val salt3 = Server(serverConfig().apply { settingsStore = Storage.Memory() }).use { it.storage.salt } Assert.assertFalse(salt1.contentEquals(salt2)) Assert.assertFalse(salt1.contentEquals(salt3)) @@ -87,14 +82,14 @@ class StorageTest : BaseTest() { fun propFileTest() { val file = File("test.db").absoluteFile - val salt1 = SettingsStore(Storage.Property(), KotlinLogging.logger("test1")).use { it.getSalt() } - val salt2 = SettingsStore(Storage.Property(), KotlinLogging.logger("test2")).use { it.getSalt() } + val salt1 = SettingsStore(Storage.Property(), LoggerFactory.getLogger("test1")).use { it.salt } + val salt2 = SettingsStore(Storage.Property(), LoggerFactory.getLogger("test2")).use { it.salt } Assert.assertArrayEquals(salt1, salt2) file.delete() - val salt3 = Server(serverConfig().apply { settingsStore = Storage.Property().file(file) }).use { it.storage.getSalt() } - val salt4 = Server(serverConfig().apply { settingsStore = Storage.Property().file(file) }).use { it.storage.getSalt() } + val salt3 = Server(serverConfig().apply { settingsStore = Storage.Property().file(file) }).use { it.storage.salt } + val salt4 = Server(serverConfig().apply { settingsStore = Storage.Property().file(file) }).use { it.storage.salt } Assert.assertArrayEquals(salt3, salt4) Assert.assertFalse(salt1.contentEquals(salt4)) diff --git a/test/dorkboxTest/network/StreamingTest.kt b/test/dorkboxTest/network/StreamingTest.kt index 858339b5..75f61bd2 100644 --- a/test/dorkboxTest/network/StreamingTest.kt +++ b/test/dorkboxTest/network/StreamingTest.kt @@ -1,57 +1,202 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network +import dorkbox.bytes.sha256 import dorkbox.network.Client import dorkbox.network.Server import dorkbox.network.connection.Connection import dorkbox.network.connection.ConnectionParams +import dorkbox.util.Sys import org.agrona.ExpandableDirectByteBuffer import org.junit.Assert import org.junit.Test +import java.io.File import java.security.SecureRandom class StreamingTest : BaseTest() { @Test fun sendStreamingObject() { - val sizeToTest = ExpandableDirectByteBuffer.MAX_BUFFER_LENGTH / 8 + // if this number is too high, we will run out of memory + // ExpandableDirectByteBuffer.MAX_BUFFER_LENGTH = 1073741824 + val sizeToTest = ExpandableDirectByteBuffer.MAX_BUFFER_LENGTH / 32 val hugeData = ByteArray(sizeToTest) SecureRandom().nextBytes(hugeData) - run { + val server = run { val configuration = serverConfig() val server: Server = Server(configuration) addEndPoint(server) - server.bind() server.onMessage { - println("received data, shutting down!") + logger.error("received data, shutting down!") Assert.assertEquals(sizeToTest, it.size) Assert.assertArrayEquals(hugeData, it) stopEndPoints() } + server } - run { - var connectionParams: ConnectionParams? = null + val client = run { val config = clientConfig() - val client: Client = Client(config) { - connectionParams = it - Connection(it) + val client = object : Client(config) { + override fun newConnection(connectionParameters: ConnectionParams): Connection { + return Connection(connectionParameters) + } } + addEndPoint(client) client.onConnect { - val params = connectionParams ?: throw Exception("We should not have null connectionParams!") - val publication = params.connectionInfo.publication - this.endPoint.send(hugeData, publication, this) + logger.error("Sending huge data: ${Sys.getSizePretty(hugeData.size)} bytes") + send(hugeData) + logger.error("Done sending huge data: ${hugeData.size} bytes") } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) + + waitForThreads() } + + @Test + fun sendRmiStreamingObject() { + // if this number is too high, we will run out of memory + // ExpandableDirectByteBuffer.MAX_BUFFER_LENGTH = 1073741824 + val sizeToTest = ExpandableDirectByteBuffer.MAX_BUFFER_LENGTH / 32 + val hugeData = ByteArray(sizeToTest) + SecureRandom().nextBytes(hugeData) + + + val server = run { + val configuration = serverConfig() + configuration.serialization.rmi.register(TestStream::class.java, TestStreamCow::class.java) + + val server: Server = Server(configuration) + addEndPoint(server) + + server.onInit { + rmi.save(TestStreamCow(this@StreamingTest, hugeData), 765) + } + + server + } + + val client = run { + val config = clientConfig() + + val client = Client(config) + addEndPoint(client) + + client.onConnect { + logger.error("Sending huge data: ${Sys.getSizePretty(hugeData.size)} bytes") + val remote = rmi.get(765) + dorkbox.network.rmi.RemoteObject.cast(remote).async = true + remote.send(hugeData) + logger.error("Done sending huge data: ${hugeData.size} bytes") + } + + client + } + + server.bind(2000) + client.connect(LOCALHOST, 2000) + + + waitForThreads() + } + + @Test + fun sendRmiFile() { + val file = File("LICENSE") + + val server = run { + val configuration = serverConfig() + configuration.serialization.rmi.register(TestStream::class.java, TestStreamCow::class.java) + + val server: Server = Server(configuration) + addEndPoint(server) + + server.onInit { + rmi.save(TestStreamCow(this@StreamingTest, null, file), 765) + } + + server + } + + val client = run { + val config = clientConfig() + + val client = Client(config) + addEndPoint(client) + + client.onConnect { + logger.error("Sending file: $file") + val remote = rmi.get(765) + remote.send(file) + logger.error("Done sending file: $file") + } + + client + } + + server.bind(2000) + client.connect(LOCALHOST, 2000) + + + waitForThreads() + } + +} + +interface TestStream { + fun send(byteArray: ByteArray) + fun send(file: File) +} + +class TestStreamCow(val unitTest: BaseTest, val hugeData: ByteArray? = null, val file: File? = null) : TestStream { + override fun send(byteArray: ByteArray) { + // not used + } + + override fun send(file: File) { + // not used + } + + fun send(connection: Connection, byteArray: ByteArray) { + connection.logger.error("received data, shutting down!") + Assert.assertEquals(hugeData!!.size, byteArray.size) + Assert.assertArrayEquals(hugeData, byteArray) + unitTest.stopEndPoints() + } + + fun send(connection: Connection, file: File) { + connection.logger.error("received data, shutting down!") + connection.logger.error("FILE: $file") + Assert.assertArrayEquals(this.file!!.sha256(), file.sha256()) + file.delete() + unitTest.stopEndPoints() + } } diff --git a/test/dorkboxTest/network/AeronClient.kt b/test/dorkboxTest/network/app/AeronClient.kt similarity index 84% rename from test/dorkboxTest/network/AeronClient.kt rename to test/dorkboxTest/network/app/AeronClient.kt index d88ae8a1..09f2f8a9 100644 --- a/test/dorkboxTest/network/AeronClient.kt +++ b/test/dorkboxTest/network/app/AeronClient.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkboxTest.network +package dorkboxTest.network.app import ch.qos.logback.classic.Level import ch.qos.logback.classic.Logger @@ -21,11 +21,9 @@ import ch.qos.logback.classic.encoder.PatternLayoutEncoder import ch.qos.logback.classic.joran.JoranConfigurator import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.ConsoleAppender -import dorkbox.netUtil.IPv4 import dorkbox.network.Client import dorkbox.network.ClientConfiguration import dorkbox.network.connection.Connection -import dorkbox.network.ipFilter.IpSubnetFilterRule import dorkbox.storage.Storage import org.slf4j.LoggerFactory import sun.misc.Unsafe @@ -33,10 +31,6 @@ import java.lang.reflect.Field import java.text.SimpleDateFormat import java.util.* -/** - * - */ -@Suppress("UNUSED_ANONYMOUS_PARAMETER") object AeronClient { init { @@ -108,43 +102,38 @@ object AeronClient { fun main(args: Array) { val configuration = ClientConfiguration() configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk! - configuration.port = 2000 -// configuration.enableIpc = true configuration.enableIpc = false configuration.enableIPv4 = true -// configuration.enableIPv4 = false -// configuration.enableIPv6 = true -// configuration.uniqueAeronDirectory = true + configuration.enableIPv6 = false - val client = Client(configuration) + configuration.uniqueAeronDirectory = true - client.filter(IpSubnetFilterRule(IPv4.LOCALHOST, 32)) + val client = Client(configuration) - client.filter { - println("should this connection be allowed?") - true + client.onInit { + logger.error("initialized") } client.onConnect { - println("connected") + logger.error("connected") + send("HI THERE!") } client.onDisconnect { - println("disconnect") + logger.error("disconnect") } client.onError { throwable -> - println("has error") + logger.error("has error") throwable.printStackTrace() } client.onMessage { message -> - println("HAS MESSAGE!") - println(message) + logger.error("HAS MESSAGE! $message") } - client.connect("127.0.0.1") // UDP connection via loopback + client.connect("127.0.0.1", 2000) // UDP connection via loopback // different ones needed @@ -168,7 +157,9 @@ object AeronClient { // on server, a connection local RMI object "uses" an id for global, so there will never be a conflict // using some tricks, we can make it so that it DOESN'T matter the order in which objects are created, // and can specify, if we want, the object created. - // Once created though, as NEW ONE with the same ID cannot be created until the old one is removed! + // Once created though, as NEW ONE with the same ID cannot be created until the old one is removed! + + Thread.sleep(2000L) client.close() } } diff --git a/test/dorkboxTest/network/app/AeronClientServer.kt b/test/dorkboxTest/network/app/AeronClientServer.kt new file mode 100644 index 00000000..6a6ac141 --- /dev/null +++ b/test/dorkboxTest/network/app/AeronClientServer.kt @@ -0,0 +1,244 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network.app + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.joran.JoranConfigurator +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.ConsoleAppender +import dorkbox.netUtil.IPv4 +import dorkbox.network.Client +import dorkbox.network.ClientConfiguration +import dorkbox.network.Server +import dorkbox.network.ServerConfiguration +import dorkbox.network.connection.Connection +import dorkbox.network.ipFilter.IpSubnetFilterRule +import dorkbox.storage.Storage +import org.slf4j.LoggerFactory +import sun.misc.Unsafe +import java.lang.reflect.Field + +/** + * + */ +@Suppress("UNUSED_ANONYMOUS_PARAMETER") +class AeronClientServer { + companion object { + init { + try { + val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe") + theUnsafe.isAccessible = true + val u = theUnsafe.get(null) as Unsafe + val cls = Class.forName("jdk.internal.module.IllegalAccessLogger") + val logger: Field = cls.getDeclaredField("logger") + u.putObjectVolatile(cls, u.staticFieldOffset(logger), null) + } catch (e: NoSuchFieldException) { + e.printStackTrace() + } catch (e: IllegalAccessException) { + e.printStackTrace() + } catch (e: ClassNotFoundException) { + e.printStackTrace() + } + + // assume SLF4J is bound to logback in the current environment + val rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger + val context = rootLogger.loggerContext + val jc = JoranConfigurator() + jc.context = context + context.reset() // override default configuration + +// rootLogger.setLevel(Level.OFF); + + // rootLogger.setLevel(Level.INFO); +// rootLogger.level = Level.DEBUG + rootLogger.level = Level.TRACE +// rootLogger.setLevel(Level.ALL); + + + // we only want error messages + val nettyLogger = LoggerFactory.getLogger("io.netty") as Logger + nettyLogger.level = Level.ERROR + + // we only want error messages + val kryoLogger = LoggerFactory.getLogger("com.esotericsoftware") as Logger + kryoLogger.level = Level.ERROR + + + val encoder = PatternLayoutEncoder() + encoder.context = context + encoder.pattern = "%date{HH:mm:ss.SSS} %-5level [%logger{35}] %msg%n" + encoder.start() + + val consoleAppender = ConsoleAppender() + consoleAppender.context = context + consoleAppender.encoder = encoder + consoleAppender.start() + + rootLogger.addAppender(consoleAppender) + } + + /** + * Command-line entry point. + * + * @param args Command-line arguments + * + * @throws Exception On any error + */ + @Throws(Exception::class) + @JvmStatic + fun main(args: Array) { + val acs = AeronClientServer() + + if (args.contains("client")) { + acs.client("172.31.79.129") + } else if (args.contains("server")) { + val server = acs.server() + server.waitForClose() + } else { + acs.server() + acs.client("localhost") + } + } + } + + + fun client(remoteAddress: String) { + val configuration = ClientConfiguration() + configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk! + configuration.appId = "aeron_test" + + configuration.enableIpc = false +// configuration.enableIPv4 = false + configuration.enableIPv6 = false + +// configuration.uniqueAeronDirectory = true + + val client = Client(configuration) + + client.onInit { + logger.error("initialized") + } + + client.onConnect { + logger.error("connected") + send("HI THERE!") + } + + client.onDisconnect { + logger.error("disconnect") + } + + client.onError { throwable -> + logger.error("has error") + throwable.printStackTrace() + } + + client.onMessage { message -> + logger.error("HAS MESSAGE! $message") + } + + client.connect(remoteAddress, 2000) // UDP connection via loopback + + + // different ones needed + // send - reliable + // send - unreliable + // send - priority (0-255 -- 255 is MAX priority) when sending, max is always sent immediately, then lower priority is sent if there is no backpressure from the MediaDriver. + // send - IPC/local +// runBlocking { +// while (!client.isShutdown()) { +// client.send("ECHO " + java.lang.Long.toUnsignedString(client.crypto.secureRandom.nextLong(), 16)) +// } +// } + + + // connection needs to know + // is UDP or IPC + // host address + + // RMI + // client.get(5) -> gets from the server connection, if exists, then global. + // on server, a connection local RMI object "uses" an id for global, so there will never be a conflict + // using some tricks, we can make it so that it DOESN'T matter the order in which objects are created, + // and can specify, if we want, the object created. + // Once created though, as NEW ONE with the same ID cannot be created until the old one is removed! + + Thread.sleep(2000L) + client.close() + } + + + fun server(): Server { + val configuration = ServerConfiguration() + configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk! + configuration.listenIpAddress = "*" + configuration.maxClientCount = 50 + configuration.appId = "aeron_test" + + configuration.enableIpc = false +// configuration.enableIPv4 = false + configuration.enableIPv6 = false + + configuration.maxConnectionsPerIpAddress = 50 + + val server = Server(configuration) + + // we must always make sure that aeron is shut-down before starting again. + if (!server.ensureStopped()) { + throw IllegalStateException("Aeron was unable to shut down in a timely manner.") + } + + server.filter(IpSubnetFilterRule(IPv4.LOCALHOST, 32)) + + server.filter { clientAddress, tagName -> + println("should the connection $clientAddress be allowed?") + true + } + + server.onInit { + logger.error("initialized") + } + + server.onConnect { + logger.error("connected: $this") + } + + server.onDisconnect { + logger.error("disconnect: $this") + } + + server.onErrorGlobal { throwable -> + server.logger.error("from test: has error") + throwable.printStackTrace() + } + + server.onError { throwable -> + logger.error("from test: has connection error: $this") + throwable.printStackTrace() + } + + server.onMessage { message -> + logger.error("got message! $message") + send("ECHO $message") + } + + server.bind(2000) + + return server + } +} diff --git a/test/dorkboxTest/network/app/AeronClientServerForever.kt b/test/dorkboxTest/network/app/AeronClientServerForever.kt new file mode 100644 index 00000000..265c5d7a --- /dev/null +++ b/test/dorkboxTest/network/app/AeronClientServerForever.kt @@ -0,0 +1,254 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network.app + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.joran.JoranConfigurator +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.ConsoleAppender +import dorkbox.netUtil.IPv4 +import dorkbox.network.Client +import dorkbox.network.ClientConfiguration +import dorkbox.network.Server +import dorkbox.network.ServerConfiguration +import dorkbox.network.connection.Connection +import dorkbox.network.ipFilter.IpSubnetFilterRule +import dorkbox.storage.Storage +import dorkbox.util.Sys +import kotlinx.atomicfu.atomic +import org.slf4j.LoggerFactory +import sun.misc.Unsafe +import java.lang.reflect.Field + +/** + * THIS WILL RUN FOREVER. It is primarily used for profiling. + */ +class AeronClientServerForever { + companion object { + init { + try { + val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe") + theUnsafe.isAccessible = true + val u = theUnsafe.get(null) as Unsafe + val cls = Class.forName("jdk.internal.module.IllegalAccessLogger") + val logger: Field = cls.getDeclaredField("logger") + u.putObjectVolatile(cls, u.staticFieldOffset(logger), null) + } catch (e: NoSuchFieldException) { + e.printStackTrace() + } catch (e: IllegalAccessException) { + e.printStackTrace() + } catch (e: ClassNotFoundException) { + e.printStackTrace() + } + + // assume SLF4J is bound to logback in the current environment + val rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger + val context = rootLogger.loggerContext + val jc = JoranConfigurator() + jc.context = context + context.reset() // override default configuration + +// rootLogger.setLevel(Level.OFF); + + // rootLogger.setLevel(Level.INFO); + rootLogger.level = Level.DEBUG +// rootLogger.level = Level.TRACE +// rootLogger.setLevel(Level.ALL); + + + // we only want error messages + val nettyLogger = LoggerFactory.getLogger("io.netty") as Logger + nettyLogger.level = Level.ERROR + + // we only want error messages + val kryoLogger = LoggerFactory.getLogger("com.esotericsoftware") as Logger + kryoLogger.level = Level.ERROR + + + val encoder = PatternLayoutEncoder() + encoder.context = context + encoder.pattern = "%date{HH:mm:ss.SSS} %-5level [%logger{35}] %msg%n" + encoder.start() + + val consoleAppender = ConsoleAppender() + consoleAppender.context = context + consoleAppender.encoder = encoder + consoleAppender.start() + + rootLogger.addAppender(consoleAppender) + } + + /** + * Command-line entry point. + * + * @param args Command-line arguments + * + * @throws Exception On any error + */ + @Throws(Exception::class) + @JvmStatic + fun main(args: Array) { + val acs = AeronClientServerForever() + val server = acs.server() + val client = acs.client("localhost") + + server.waitForClose() + } + } + + + fun client(remoteAddress: String) { + val count = atomic(0L) + val time = Stopwatch.createUnstarted() + + val configuration = ClientConfiguration() + configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk! + configuration.appId = "aeron_test" + + configuration.enableIpc = false +// configuration.enableIPv4 = false + configuration.enableIPv6 = false + +// configuration.uniqueAeronDirectory = true + + val client = Client(configuration) + + client.onInit { + logger.error("initialized") + } + + client.onConnect { + logger.error("connected") + time.start() + send(NoGarbageObj()) + } + + client.onDisconnect { + logger.error("disconnect") + } + + client.onError { throwable -> + logger.error("has error") + throwable.printStackTrace() + } + + client.onMessage { message -> + val andIncrement = count.getAndIncrement() + if ((andIncrement % 100000) == 0L) { + logger.error("Sending messages: $andIncrement") + } + if (andIncrement > 0 && (andIncrement % 500000) == 0L) { + // we are measuring roundtrip performance + logger.error("For 1,000,000 messages: ${Sys.getTimePrettyFull(time.elapsedNanos())}") + time.reset() + time.start() + } + + send(message) + } + + client.connect(remoteAddress, 2000) // UDP connection via loopback + + + // different ones needed + // send - reliable + // send - unreliable + // send - priority (0-255 -- 255 is MAX priority) when sending, max is always sent immediately, then lower priority is sent if there is no backpressure from the MediaDriver. + // send - IPC/local +// runBlocking { +// while (!client.isShutdown()) { +// client.send("ECHO " + java.lang.Long.toUnsignedString(client.crypto.secureRandom.nextLong(), 16)) +// } +// } + + + // connection needs to know + // is UDP or IPC + // host address + + // RMI + // client.get(5) -> gets from the server connection, if exists, then global. + // on server, a connection local RMI object "uses" an id for global, so there will never be a conflict + // using some tricks, we can make it so that it DOESN'T matter the order in which objects are created, + // and can specify, if we want, the object created. + // Once created though, as NEW ONE with the same ID cannot be created until the old one is removed! + +// Thread.sleep(2000L) +// client.close() + } + + + fun server(): Server { + val configuration = ServerConfiguration() + configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk! + configuration.listenIpAddress = "*" + configuration.maxClientCount = 50 + configuration.appId = "aeron_test" + + configuration.enableIpc = false +// configuration.enableIPv4 = false + configuration.enableIPv6 = false + + configuration.maxConnectionsPerIpAddress = 50 + configuration.serialization.register(NoGarbageObj::class.java, NGOSerializer()) + + val server = Server(configuration) + + // we must always make sure that aeron is shut-down before starting again. + if (!server.ensureStopped()) { + throw IllegalStateException("Aeron was unable to shut down in a timely manner.") + } + + server.filter(IpSubnetFilterRule(IPv4.LOCALHOST, 32)) + + server.filter { clientAddress, tagName -> + println("should the connection $clientAddress be allowed?") + true + } + + server.onInit { + logger.error("initialized") + } + + server.onConnect { + logger.error("connected: $this") + } + + server.onDisconnect { + logger.error("disconnect: $this") + } + + server.onErrorGlobal { throwable -> + server.logger.error("from test: has error") + throwable.printStackTrace() + } + + server.onError { throwable -> + logger.error("from test: has connection error: $this") + throwable.printStackTrace() + } + + server.onMessage { message -> + send(message) + } + + server.bind(2000) + + return server + } +} diff --git a/test/dorkboxTest/network/app/AeronClientServerRMIForever.kt b/test/dorkboxTest/network/app/AeronClientServerRMIForever.kt new file mode 100644 index 00000000..992a9cde --- /dev/null +++ b/test/dorkboxTest/network/app/AeronClientServerRMIForever.kt @@ -0,0 +1,303 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network.app + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.joran.JoranConfigurator +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.ConsoleAppender +import dorkbox.netUtil.IPv4 +import dorkbox.network.Client +import dorkbox.network.ClientConfiguration +import dorkbox.network.Server +import dorkbox.network.ServerConfiguration +import dorkbox.network.connection.Connection +import dorkbox.network.ipFilter.IpSubnetFilterRule +import dorkbox.network.rmi.RemoteObject +import dorkbox.storage.Storage +import dorkbox.util.Sys +import kotlinx.atomicfu.atomic +import org.slf4j.LoggerFactory +import sun.misc.Unsafe +import java.lang.reflect.Field + +/** + * THIS WILL RUN FOREVER. It is primarily used for profiling. + */ +class AeronClientServerRMIForever { + companion object { + init { + try { + val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe") + theUnsafe.isAccessible = true + val u = theUnsafe.get(null) as Unsafe + val cls = Class.forName("jdk.internal.module.IllegalAccessLogger") + val logger: Field = cls.getDeclaredField("logger") + u.putObjectVolatile(cls, u.staticFieldOffset(logger), null) + } catch (e: NoSuchFieldException) { + e.printStackTrace() + } catch (e: IllegalAccessException) { + e.printStackTrace() + } catch (e: ClassNotFoundException) { + e.printStackTrace() + } + + // assume SLF4J is bound to logback in the current environment + val rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger + val context = rootLogger.loggerContext + val jc = JoranConfigurator() + jc.context = context + context.reset() // override default configuration + +// rootLogger.setLevel(Level.OFF); + + // rootLogger.setLevel(Level.INFO); + rootLogger.level = Level.DEBUG +// rootLogger.level = Level.TRACE +// rootLogger.setLevel(Level.ALL); + + + // we only want error messages + val nettyLogger = LoggerFactory.getLogger("io.netty") as Logger + nettyLogger.level = Level.ERROR + + // we only want error messages + val kryoLogger = LoggerFactory.getLogger("com.esotericsoftware") as Logger + kryoLogger.level = Level.ERROR + + + val encoder = PatternLayoutEncoder() + encoder.context = context + encoder.pattern = "%date{HH:mm:ss.SSS} %-5level [%logger{35}] %msg%n" + encoder.start() + + val consoleAppender = ConsoleAppender() + consoleAppender.context = context + consoleAppender.encoder = encoder + consoleAppender.start() + + rootLogger.addAppender(consoleAppender) + } + + /** + * Command-line entry point. + * + * @param args Command-line arguments + * + * @throws Exception On any error + */ + @Throws(Exception::class) + @JvmStatic + fun main(args: Array) { + val acs = AeronClientServerForever() + val server = acs.server() + val client = acs.client("localhost") + + server.waitForClose() + } + } + + + fun client(remoteAddress: String) { + val configuration = ClientConfiguration() + configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk! + configuration.appId = "aeron_test" + + configuration.enableIpc = false +// configuration.enableIPv4 = false + configuration.enableIPv6 = false + +// configuration.uniqueAeronDirectory = true + + + val client = Client(configuration) + + client.onInit { + logger.error("initialized") + rmi.save(GarbageObj(123), 456) + } + + client.onConnect { + logger.error("connected") + + val obj = this.rmi.get(123) + val casted = RemoteObject.cast(obj) + casted.async = true + obj.send() + } + + client.onDisconnect { + logger.error("disconnect") + } + + client.onError { throwable -> + logger.error("has error") + throwable.printStackTrace() + } + +// client.onMessage { message -> +// val andIncrement = count.getAndIncrement() +// if ((andIncrement % 100000) == 0L) { +// logger.error { "Sending messages: $andIncrement" } +// } +// if (andIncrement > 0 && (andIncrement % 500000) == 0L) { +// // we are measuring roundtrip performance +// logger.error { "For 1,000,000 messages: ${Sys.getTimePrettyFull(time.elapsedNanos())}" } +// time.reset() +// time.start() +// } +// +// send(message) +// } + + client.connect(remoteAddress, 2000) // UDP connection via loopback + + + // different ones needed + // send - reliable + // send - unreliable + // send - priority (0-255 -- 255 is MAX priority) when sending, max is always sent immediately, then lower priority is sent if there is no backpressure from the MediaDriver. + // send - IPC/local +// runBlocking { +// while (!client.isShutdown()) { +// client.send("ECHO " + java.lang.Long.toUnsignedString(client.crypto.secureRandom.nextLong(), 16)) +// } +// } + + + // connection needs to know + // is UDP or IPC + // host address + + // RMI + // client.get(5) -> gets from the server connection, if exists, then global. + // on server, a connection local RMI object "uses" an id for global, so there will never be a conflict + // using some tricks, we can make it so that it DOESN'T matter the order in which objects are created, + // and can specify, if we want, the object created. + // Once created though, as NEW ONE with the same ID cannot be created until the old one is removed! + +// Thread.sleep(2000L) +// client.close() + } + + + fun server(): Server { + val configuration = ServerConfiguration() + configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk! + configuration.listenIpAddress = "*" + configuration.maxClientCount = 50 + configuration.appId = "aeron_test" + + configuration.enableIpc = false +// configuration.enableIPv4 = false + configuration.enableIPv6 = false + + configuration.maxConnectionsPerIpAddress = 50 + configuration.serialization.register(NoGarbageObj::class.java, NGOSerializer()) + configuration.serialization.rmi.register(GarbageObjInt::class.java, GarbageObj::class.java) + + + val server = Server(configuration) + + // we must always make sure that aeron is shut-down before starting again. + if (!server.ensureStopped()) { + throw IllegalStateException("Aeron was unable to shut down in a timely manner.") + } + + server.filter(IpSubnetFilterRule(IPv4.LOCALHOST, 32)) + + server.filter { clientAddress, tagName -> + println("should the connection $clientAddress be allowed?") + true + } + + server.onInit { + logger.error("initialized") + rmi.save(GarbageObj(456), 123) + } + + server.onConnect { + logger.error("connected: $this") + } + + server.onDisconnect { + logger.error("disconnect: $this") + } + + server.onErrorGlobal { throwable -> + server.logger.error("from test: has error") + throwable.printStackTrace() + } + + server.onError { throwable -> + logger.error("from test: has connection error: $this") + throwable.printStackTrace() + } + +// server.onMessage { message -> +// send(message) +// } + + server.bind(2000) + + return server + } +} + + +interface GarbageObjInt { + fun send() +} +class GarbageObj(val otherId: Int): GarbageObjInt { + companion object { + val count = atomic(0L) + val time = Stopwatch.createUnstarted() + } + + init { + if (otherId == 123) { + time.start() + } + } + + override fun send() { + // do nothing + } + + fun send(connection: Connection) { + if (otherId == 123) { + val andIncrement = count.getAndIncrement() + if ((andIncrement % 100000) == 0L) { + connection.logger.error("Sending messages: $andIncrement") + } + if (andIncrement > 0 && (andIncrement % 500000) == 0L) { + // we are measuring roundtrip performance + connection.logger.error("For 1,000,000 messages: ${Sys.getTimePrettyFull(time.elapsedNanos())}") + time.reset() + time.start() + } + } + + val obj = connection.rmi.get(otherId) + val casted = RemoteObject.cast(obj) + casted.async = true + obj.send() + } +} + + diff --git a/test/dorkboxTest/network/app/AeronRmiClientServer.kt b/test/dorkboxTest/network/app/AeronRmiClientServer.kt new file mode 100644 index 00000000..ca7d570b --- /dev/null +++ b/test/dorkboxTest/network/app/AeronRmiClientServer.kt @@ -0,0 +1,406 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network.app + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.joran.JoranConfigurator +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.ConsoleAppender +import dorkbox.network.* +import dorkbox.network.connection.Connection +import dorkbox.storage.Storage +import dorkbox.util.Sys +import dorkboxTest.network.rmi.cows.TestCow +import dorkboxTest.network.rmi.cows.TestCowImpl +import io.aeron.driver.ThreadingMode +import kotlinx.coroutines.* +import org.agrona.ExpandableDirectByteBuffer +import org.agrona.concurrent.NoOpIdleStrategy +import org.agrona.concurrent.SigInt +import org.slf4j.LoggerFactory +import sun.misc.Unsafe +import java.lang.reflect.Field +import java.security.SecureRandom +import java.util.concurrent.* +import java.util.concurrent.atomic.* + +/** + * + */ +class AeronRmiClientServer { + companion object { + private val counter = AtomicInteger(0) + + init { + try { + val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe") + theUnsafe.isAccessible = true + val u = theUnsafe.get(null) as Unsafe + val cls = Class.forName("jdk.internal.module.IllegalAccessLogger") + val logger: Field = cls.getDeclaredField("logger") + u.putObjectVolatile(cls, u.staticFieldOffset(logger), null) + } catch (e: NoSuchFieldException) { + e.printStackTrace() + } catch (e: IllegalAccessException) { + e.printStackTrace() + } catch (e: ClassNotFoundException) { + e.printStackTrace() + } + + // assume SLF4J is bound to logback in the current environment + val rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger + val context = rootLogger.loggerContext + val jc = JoranConfigurator() + jc.context = context + context.reset() // override default configuration + +// rootLogger.setLevel(Level.OFF); + + // rootLogger.setLevel(Level.INFO); +// rootLogger.level = Level.DEBUG +// rootLogger.level = Level.TRACE +// rootLogger.setLevel(Level.ALL); + + + // we only want error messages + val nettyLogger = LoggerFactory.getLogger("io.netty") as Logger + nettyLogger.level = Level.ERROR + + // we only want error messages + val kryoLogger = LoggerFactory.getLogger("com.esotericsoftware") as Logger + kryoLogger.level = Level.ERROR + + + val encoder = PatternLayoutEncoder() + encoder.context = context + encoder.pattern = "%date{HH:mm:ss.SSS} [%t] %-5level [%logger{35}] %msg%n" + encoder.start() + + val consoleAppender = ConsoleAppender() + consoleAppender.context = context + consoleAppender.encoder = encoder + consoleAppender.start() + + rootLogger.addAppender(consoleAppender) + } + + /** + * Command-line entry point. + * + * @param args Command-line arguments + * + * @throws Exception On any error + */ + @Throws(Exception::class) + @JvmStatic + fun main(cliArgs: Array) { + + val config = Config() + + val configProcessor = dorkbox.config.ConfigProcessor(config) + .envPrefix("") + .cliArguments(cliArgs) + .process() + + fun client(acs: AeronRmiClientServer): Client { + val client = acs.client(0) + + client.onDisconnect { + logger.error("Disconnect -> Reconnect...") + client.reconnect() + } + + client.onConnect { + logger.error("Starting test...") + val secureRandom = SecureRandom() + val sizeToTest = ExpandableDirectByteBuffer.MAX_BUFFER_LENGTH / 32 + + // don't want to time allocating the mem, just time "serializing and sending" + val hugeData = ByteArray(sizeToTest) + secureRandom.nextBytes(hugeData) // REALLY slow!!! + + var count = 0 + var timed = 0L + + client.logger.error("Initializing test.") + + // just to start it up. + repeat(15) { + if (!client.send(hugeData)) { + client.logger.error("Unable to send data!") + return@onConnect + } + } + + client.logger.error("Starting test.") + + + val allStopwatch = Stopwatch.createStarted() + while (TimeUnit.NANOSECONDS.toSeconds(timed) < 5) { +// client.logger.error { "Starting round: $count" } + + val roundStopwatch = Stopwatch.createStarted() + client.send(hugeData) + timed += roundStopwatch.elapsedNanos() +// client.logger.error { "Finished round $count in: $roundStopwatch" } + count++ + } + + + val amountInMB = (count.toLong()*sizeToTest)/Sys.MEGABYTE + val amountInmb = (count.toLong()*sizeToTest*8)/Sys.MEGABYTE + val fullElapsed = allStopwatch.elapsedNanos() + + client.logger.error("Finished $count rounds in: ${Sys.getTimePrettyFull(fullElapsed)}") + client.logger.error("Sending data portion took: ${Sys.getTimePrettyFull(timed)} for $amountInMB MB") + + val timedInSeconds = TimeUnit.NANOSECONDS.toSeconds(timed) + client.logger.error("Rate is: ${amountInMB/timedInSeconds} MB/s") + client.logger.error("Rate is: ${amountInmb/timedInSeconds} mb/s") + } + + return client + } + + + val acs = AeronRmiClientServer() + try { + if (config.server) { + val server = acs.server() + + server.onMessage { + logger.error("Received Byte array!") + } + + server.bind(2000, 2001) + server.waitForClose() + } + + else if (config.client) { + val client = client(acs) + client.connect(config.ip, 2000, 2001, 0) // UDP connection via loopback + + client.waitForClose() + client.logger.error("DONE WAITING") + } else { + val server = acs.server() + server.onMessage { + // let the client send us data + } + + val client = client(acs) + + server.bindIpc() + client.connectIpc() + + client.waitForClose() + client.logger.error("DONE WAITING") + } + + } catch (e: Exception) { + e.printStackTrace() + println("WHOOPS") + } + } + } + + + fun client(index: Int): Client { + val configuration = ClientConfiguration() + config(configuration) + + val client = Client(configuration) + + client.onInit { + logger.error("$index: initialized") + } + + client.onConnect { + logger.error("$index: connected") + val remoteObject = rmi.get(1) +// val remoteObjec2 = rmi.getGlobal(44) +// if (index == 9) { +// println("PROBLEMS!!") +// } +// logger.error("$index: starting dispatch") +// try { +// GlobalScope.async(Dispatchers.Default) { +// var startTime = System.nanoTime() +// logger.error("$index: started dispatch") +// +// var previousCount = 0 +// while (true) { +// val counter = counter.getAndIncrement() +// try { +// // ping() +// // RemoteObject.cast(remoteObject).async { +// val value = "$index" +// val mooTwoValue = remoteObject.mooTwo(value) +// if (mooTwoValue != "moo-two: $value") { +// throw Exception("Value not the same!") +// } +// // remoteObject.mooTwo("count $counter") +// // } +// } catch (e: Exception) { +// e.printStackTrace() +// logger.error { "$index: ERROR with client " } +// return@async +// } +// +// val elapsedTime = ( System.nanoTime() - startTime) / 1_000_000_000.0 +// if (index == 0 && elapsedTime > 1.0) { +// logger.error { +// val perSecond = ((counter - previousCount) / elapsedTime).toInt() +// "Count: $perSecond/sec" } +// startTime = System.nanoTime() +// previousCount = counter +// } +// } +// } +// } catch (e: Exception) { +// e.printStackTrace() +// } + } + + client.onError { throwable -> + logger.error("***has error***") + throwable.printStackTrace() + } + + client.onMessage { message -> + logger.error("HAS MESSAGE! $message") + } + +// SigInt.register { +// client.logger.info { "Shutting down via sig-int command" } +// runBlocking { +// client.close(closeEverything = true, initiatedByClientClose = false, initiatedByShutdown = false) +// } +// } + + + return client + } + + fun config(config: Configuration) { + if (config is ServerConfiguration) { + config.settingsStore = Storage.Property().file("config.json") + } else { + // don't want to persist anything on disk! + config.settingsStore = Storage.Memory() + } + + config.appId = "aeron_test" + config.enableIPv6 = false + + config.enableIpc = true +// config.enableIpc = false +// config.uniqueAeronDirectory = true + +// config.forceAllowSharedAeronDriver = true + + // dedicate more **OOMPF** to the network + config.threadingMode = ThreadingMode.SHARED_NETWORK +// config.threadingMode = ThreadingMode.DEDICATED + config.pollIdleStrategy = NoOpIdleStrategy.INSTANCE + config.sendIdleStrategy = NoOpIdleStrategy.INSTANCE + + + // only if there are enough threads on the box! + if (Runtime.getRuntime().availableProcessors() >= 4) { + // config.conductorIdleStrategy = BusySpinIdleStrategy.INSTANCE + // config.sharedIdleStrategy = NoOpIdleStrategy.INSTANCE + config.receiverIdleStrategy = NoOpIdleStrategy.INSTANCE + config.senderIdleStrategy = NoOpIdleStrategy.INSTANCE + } + + + // https://blah.cloud/networks/test-jumbo-frames-working/ + // This must be a multiple of 32, and we leave some space for headers/etc + config.networkMtuSize = 8192 + config.ipcMtuSize = io.aeron.driver.Configuration.MAX_UDP_PAYLOAD_LENGTH + + // 4 MB for receive + config.receiveBufferSize = 4194304 + + // recommended for 10gbps networks + config.initialWindowLength = io.aeron.driver.Configuration.INITIAL_WINDOW_LENGTH_DEFAULT + + config.maxStreamSizeInMemoryMB = 128 + } + + + fun server(): Server { + val configuration = ServerConfiguration() + config(configuration) + + configuration.listenIpAddress = "*" + configuration.maxClientCount = 50 + configuration.maxConnectionsPerIpAddress = 50 + + configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java) + + val server = Server(configuration) + +// server.rmiGlobal.save(TestCowImpl(44), 44) + + // we must always make sure that aeron is shut-down before starting again. + if (!server.ensureStopped()) { + throw IllegalStateException("Aeron was unable to shut down in a timely manner.") + } + + server.onInit { + logger.error("initialized") + rmi.save(TestCowImpl(1), 1) + } + + server.onConnect { + logger.error("connected: $this") + } + + server.onDisconnect { + logger.error("disconnect: $this") + } + + server.onErrorGlobal { throwable -> + server.logger.error("from test: has error") + throwable.printStackTrace() + } + + server.onError { throwable -> + logger.error("from test: has connection error: $this") + throwable.printStackTrace() + } + + + var closeCalled = false + SigInt.register { + // only close once + if (!closeCalled) { + closeCalled = true + server.logger.info("Shutting down via sig-int command") + server.close() + } + } + + + return server + } +} + + diff --git a/test/dorkboxTest/network/AeronServer.kt b/test/dorkboxTest/network/app/AeronServer.kt similarity index 81% rename from test/dorkboxTest/network/AeronServer.kt rename to test/dorkboxTest/network/app/AeronServer.kt index 28d6e9d7..f922d7c5 100644 --- a/test/dorkboxTest/network/AeronServer.kt +++ b/test/dorkboxTest/network/app/AeronServer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkboxTest.network +package dorkboxTest.network.app import ch.qos.logback.classic.Level import ch.qos.logback.classic.Logger @@ -25,7 +25,6 @@ import dorkbox.network.Server import dorkbox.network.ServerConfiguration import dorkbox.network.connection.Connection import dorkbox.storage.Storage -import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory import sun.misc.Unsafe import java.lang.reflect.Field @@ -88,57 +87,58 @@ object AeronServer { fun main(args: Array) { val configuration = ServerConfiguration() configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk! - configuration.listenIpAddress = "127.0.0.1" - configuration.port = 2000 + configuration.listenIpAddress = "*" +// configuration.listenIpAddress = "127.0.0.1" configuration.maxClientCount = 50 // configuration.enableIpc = true -// configuration.enableIpc = false -// configuration.enableIPv4 = false - configuration.enableIPv6 = false + configuration.enableIpc = false + configuration.enableIPv4 = true + configuration.enableIPv6 = true configuration.maxConnectionsPerIpAddress = 50 val server: Server<*> = Server(configuration) // we must always make sure that aeron is shut-down before starting again. - while (server.isRunning()) { - server.logger.error("Aeron was still running. Waiting for it to stop...") - Thread.sleep(2000) + if (!server.ensureStopped()) { + throw IllegalStateException("Aeron was unable to shut down in a timely manner.") } - - server.filter { - println("should the connection $this be allowed?") + server.filter { clientAddress, tagName -> + println("should the connection $clientAddress be allowed?") true } + server.onInit { + logger.error("initialized") + } + server.onConnect { - println("connected: $this") + logger.error("connected: $this") } server.onDisconnect { - println("disconnect: $this") + logger.error("disconnect: $this") } server.onErrorGlobal { throwable -> - println("from test: has error") + server.logger.error("from test: has error") throwable.printStackTrace() } server.onError { throwable -> - println("from test: has connection error: $this") + logger.error("from test: has connection error: $this") throwable.printStackTrace() } server.onMessage { message -> + logger.error("got message! $message") send("ECHO $message") } - server.bind() + server.bind(2000) - runBlocking { - server.waitForClose() - } + server.waitForClose() } } diff --git a/test/dorkboxTest/network/app/Config.kt b/test/dorkboxTest/network/app/Config.kt new file mode 100644 index 00000000..cf10dc4d --- /dev/null +++ b/test/dorkboxTest/network/app/Config.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network.app + +internal class Config { + var ip = "127.0.0.1" + + var server = false + var client = false +} diff --git a/test/dorkboxTest/network/app/NGOSerializer.kt b/test/dorkboxTest/network/app/NGOSerializer.kt new file mode 100644 index 00000000..b42ad659 --- /dev/null +++ b/test/dorkboxTest/network/app/NGOSerializer.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network.app + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output + +class NGOSerializer: Serializer() { + val data = NoGarbageObj() + override fun write(kryo: Kryo, output: Output, `object`: NoGarbageObj) { + } + + override fun read(kryo: Kryo, input: Input, type: Class): NoGarbageObj { + return data + } + +} diff --git a/test/dorkboxTest/network/app/NoGarbageObj.kt b/test/dorkboxTest/network/app/NoGarbageObj.kt new file mode 100644 index 00000000..4f2ed314 --- /dev/null +++ b/test/dorkboxTest/network/app/NoGarbageObj.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network.app + +class NoGarbageObj { + val data = "I'm not a witch!" +} diff --git a/test/dorkboxTest/network/app/Stopwatch.kt b/test/dorkboxTest/network/app/Stopwatch.kt new file mode 100644 index 00000000..20dd994d --- /dev/null +++ b/test/dorkboxTest/network/app/Stopwatch.kt @@ -0,0 +1,267 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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. + */ + +/* + * Copyright (C) 2008 The Guava 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 + * + * 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 dorkboxTest.network.app + +import java.time.Duration +import java.util.concurrent.* + +/** + * An object that measures elapsed time in nanoseconds. It is useful to measure elapsed time using + * this class instead of direct calls to [System.nanoTime] for a few reasons: + * + * + * * An alternate time source can be substituted, for testing or performance reasons. + * * As documented by `nanoTime`, the value returned has no absolute meaning, and can only + * be interpreted as relative to another timestamp returned by `nanoTime` at a different + * time. `Stopwatch` is a more effective abstraction because it exposes only these + * relative values, not the absolute ones. + * + * + * + * Basic usage: + * + *

`Stopwatch stopwatch = Stopwatch.createStarted();
+ * doSomething();
+ * stopwatch.stop(); // optional
+ *
+ * Duration duration = stopwatch.elapsed();
+ *
+ * log.info("time: " + stopwatch); // formatted string like "12.3 ms"
+`
* + * + * + * Stopwatch methods are not idempotent; it is an error to start or stop a stopwatch that is + * already in the desired state. + * + * + * When testing code that uses this class, use [.createUnstarted] or [ ][.createStarted] to supply a fake or mock ticker. This allows you to simulate any valid + * behavior of the stopwatch. + * + * + * **Note:** This class is not thread-safe. + * + * + * **Warning for Android users:** a stopwatch with default behavior may not continue to keep + * time while the device is asleep. Instead, create one like this: + * + *
`Stopwatch.createStarted(
+ * new Ticker() {
+ * public long read() {
+ * return android.os.SystemClock.elapsedRealtimeNanos();
+ * }
+ * });
+`
* + * + * @author Kevin Bourrillion + * @since 10.0 + */ +class Stopwatch { + private val ticker: Ticker + + /** + * Returns `true` if [.start] has been called on this stopwatch, and [.stop] + * has not been called since the last call to `start()`. + */ + var isRunning = false + private set + private var elapsedNanos: Long = 0 + private var startTick: Long = 0 + + internal constructor() { + ticker = Ticker.systemTicker() + } + + internal constructor(ticker: Ticker?) { + if (ticker == null) { + throw NullPointerException("ticker") + } + this.ticker = ticker + } + + /** + * Starts the stopwatch. + * + * @return this `Stopwatch` instance + * + * @throws IllegalStateException if the stopwatch is already running. + */ + fun start(): Stopwatch { + check(!isRunning) { "This stopwatch is already running." } + isRunning = true + startTick = ticker.read() + return this + } + + /** + * Stops the stopwatch. Future reads will return the fixed duration that had elapsed up to this + * point. + * + * @return this `Stopwatch` instance + * + * @throws IllegalStateException if the stopwatch is already stopped. + */ + fun stop(): Stopwatch { + val tick = ticker.read() + check(isRunning) { "This stopwatch is already stopped." } + isRunning = false + elapsedNanos += tick - startTick + return this + } + + /** + * Sets the elapsed time for this stopwatch to zero, and places it in a stopped state. + * + * @return this `Stopwatch` instance + */ + fun reset(): Stopwatch { + elapsedNanos = 0 + isRunning = false + return this + } + + fun elapsedNanos(): Long { + return if (isRunning) ticker.read() - startTick + elapsedNanos else elapsedNanos + } + + /** + * Returns the current elapsed time shown on this stopwatch, expressed in the desired time unit, + * with any fraction rounded down. + * + * + * **Note:** the overhead of measurement can be more than a microsecond, so it is generally + * not useful to specify [TimeUnit.NANOSECONDS] precision here. + * + * + * It is generally not a good idea to use an ambiguous, unitless `long` to represent + * elapsed time. Therefore, we recommend using [.elapsed] instead, which returns a + * strongly-typed [Duration] instance. + * + * @since 14.0 (since 10.0 as `elapsedTime()`) + */ + fun elapsed(desiredUnit: TimeUnit): Long { + return desiredUnit.convert(elapsedNanos(), TimeUnit.NANOSECONDS) + } + + /** + * Returns the current elapsed time shown on this stopwatch as a [Duration]. Unlike [ ][.elapsed], this method does not lose any precision due to rounding. + * + * @since 22.0 + */ + fun elapsed(): Duration { + return Duration.ofNanos(elapsedNanos()) + } + + /** + * Returns a string representation of the current elapsed time. + */ + override fun toString(): String { + return toString(elapsedNanos()) + } + + companion object { + /** + * Creates (but does not start) a new stopwatch using [System.nanoTime] as its time source. + * + * @since 15.0 + */ + fun createUnstarted(): Stopwatch { + return Stopwatch() + } + + /** + * Creates (but does not start) a new stopwatch, using the specified time source. + * + * @since 15.0 + */ + fun createUnstarted(ticker: Ticker?): Stopwatch { + return Stopwatch(ticker) + } + + /** + * Creates (and starts) a new stopwatch using [System.nanoTime] as its time source. + * + * @since 15.0 + */ + fun createStarted(): Stopwatch { + return Stopwatch().start() + } + + /** + * Creates (and starts) a new stopwatch, using the specified time source. + * + * @since 15.0 + */ + fun createStarted(ticker: Ticker?): Stopwatch { + return Stopwatch(ticker).start() + } + + fun toString(nanos: Long): String { + val unit = chooseUnit(nanos) + val value = nanos.toDouble() / TimeUnit.NANOSECONDS.convert(1, unit) + + // Too bad this functionality is not exposed as a regular method call + return String.format("%.4g %s", value, abbreviate(unit)) + } + + fun chooseUnit(nanos: Long): TimeUnit { + if (TimeUnit.DAYS.convert(nanos, TimeUnit.NANOSECONDS) > 0) { + return TimeUnit.DAYS + } + if (TimeUnit.HOURS.convert(nanos, TimeUnit.NANOSECONDS) > 0) { + return TimeUnit.HOURS + } + if (TimeUnit.MINUTES.convert(nanos, TimeUnit.NANOSECONDS) > 0) { + return TimeUnit.MINUTES + } + if (TimeUnit.SECONDS.convert(nanos, TimeUnit.NANOSECONDS) > 0) { + return TimeUnit.SECONDS + } + if (TimeUnit.MILLISECONDS.convert(nanos, TimeUnit.NANOSECONDS) > 0) { + return TimeUnit.MILLISECONDS + } + return if (TimeUnit.MICROSECONDS.convert(nanos, TimeUnit.NANOSECONDS) > 0) { + TimeUnit.MICROSECONDS + } else TimeUnit.NANOSECONDS + } + + private fun abbreviate(unit: TimeUnit): String { + return when (unit) { + TimeUnit.NANOSECONDS -> "ns" + TimeUnit.MICROSECONDS -> "\u03bcs" // μs + TimeUnit.MILLISECONDS -> "ms" + TimeUnit.SECONDS -> "s" + TimeUnit.MINUTES -> "min" + TimeUnit.HOURS -> "h" + TimeUnit.DAYS -> "d" + else -> throw AssertionError() + } + } + } +} diff --git a/test/dorkboxTest/network/app/Ticker.kt b/test/dorkboxTest/network/app/Ticker.kt new file mode 100644 index 00000000..efd0ed23 --- /dev/null +++ b/test/dorkboxTest/network/app/Ticker.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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. + *//* + * Copyright (C) 2011 The Guava 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 + * + * 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 dorkboxTest.network.app + +/** + * A time source; returns a time value representing the number of nanoseconds elapsed since some + * fixed but arbitrary point in time. Note that most users should use [Stopwatch] instead of + * interacting with this class directly. + * + * + * **Warning:** this interface can only be used to measure elapsed time, not wall time. + * + * @author Kevin Bourrillion + * @since 10.0 ([mostly + * source-compatible](https://github.com/google/guava/wiki/Compatibility) since 9.0) + */ +abstract class Ticker +/** Constructor for use by subclasses. */ +protected constructor() { + /** Returns the number of nanoseconds elapsed since this ticker's fixed point of reference. */ + abstract fun read(): Long + + companion object { + /** + * A ticker that reads the current time using [System.nanoTime]. + * + * @since 10.0 + */ + fun systemTicker(): Ticker { + return SYSTEM_TICKER + } + + private val SYSTEM_TICKER: Ticker = object : Ticker() { + override fun read(): Long { + return System.nanoTime() + } + } + } +} diff --git a/test/dorkboxTest/network/memoryTest/MemoryTest.kt b/test/dorkboxTest/network/memoryTest/MemoryTest.kt index 7edac985..617e5b3c 100644 --- a/test/dorkboxTest/network/memoryTest/MemoryTest.kt +++ b/test/dorkboxTest/network/memoryTest/MemoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,18 +20,14 @@ import dorkbox.network.Client import dorkbox.network.Server import dorkbox.network.connection.Connection import dorkbox.network.rmi.RemoteObject -import dorkbox.network.serialization.KryoExtra -import dorkbox.network.serialization.Serialization import dorkboxTest.network.BaseTest import kotlinx.coroutines.runBlocking import org.junit.Ignore import org.junit.Test -import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.* @Ignore class MemoryTest : BaseTest() { - private val counter = AtomicLong(0) - init { // the logger cannot keep-up if it's on trace setLogLevel(Level.DEBUG) @@ -39,182 +35,119 @@ class MemoryTest : BaseTest() { @Test fun runForeverIpcAsyncNormal() { - runBlocking { - val RMI_ID = 12251 + val counter = AtomicLong(0) + val RMI_ID = 12251 - run { - val configuration = serverConfig() - configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) + run { + val configuration = serverConfig() + configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) - val server = Server(configuration) - server.rmiGlobal.save(TestObjectImpl(counter), RMI_ID) - server.bind() - } + val server = Server(configuration) + server.rmiGlobal.save(TestObjectImpl(counter), RMI_ID) + server.bindIpc() + } - run { - val client = Client(clientConfig()) - client.onConnect { - val remoteObject = rmi.getGlobal(RMI_ID) - val obj = remoteObject as RemoteObject - obj.async = true - - var i = 0L - while (true) { - i++ - try { - remoteObject.setOther(i) - } catch (e: Exception) { - logger.error("Timeout when calling RMI method") - e.printStackTrace() - } + run { + val client = Client(clientConfig()) + client.onConnect { + val remoteObject = rmi.getGlobal(RMI_ID) + val obj = RemoteObject.cast(remoteObject) + obj.async = true + + var i = 0L + while (true) { + i++ + try { + remoteObject.setOther(i) + } catch (e: Exception) { + logger.error("Timeout when calling RMI method") + e.printStackTrace() } } - - client.connectIpc() } - Thread.sleep(Long.MAX_VALUE) + client.connectIpc() } + + Thread.sleep(Long.MAX_VALUE) } @Test fun runForeverIpcAsyncSuspend() { + val counter = AtomicLong(0) val RMI_ID = 12251 - runBlocking { - run { - val configuration = serverConfig() - configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) + run { + val configuration = serverConfig() + configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) - val server = Server(configuration) - server.rmiGlobal.save(TestObjectImpl(counter), RMI_ID) - server.bind() - } + val server = Server(configuration) + server.rmiGlobal.save(TestObjectImpl(counter), RMI_ID) + server.bindIpc() + } - run { - val client = Client(clientConfig()) - client.onConnect { - val remoteObject = rmi.getGlobal(RMI_ID) - val obj = remoteObject as RemoteObject - obj.async = true + run { + val client = Client(clientConfig()) + client.onConnect { + val remoteObject = rmi.getGlobal(RMI_ID) + val obj = RemoteObject.cast(remoteObject) + obj.async = true - var i = 0L - while (true) { - i++ - try { + var i = 0L + while (true) { + i++ + try { + runBlocking { remoteObject.setOtherSus(i) - } catch (e: Exception) { - logger.error("Timeout when calling RMI method") - e.printStackTrace() } + } catch (e: Exception) { + logger.error("Timeout when calling RMI method") + e.printStackTrace() } } - - client.connect() } - Thread.sleep(Long.MAX_VALUE) + client.connectIpc() } + + Thread.sleep(Long.MAX_VALUE) } @Test fun runForeverIpc() { - runBlocking { - run { - val configuration = serverConfig() + val server = run { + val configuration = serverConfig() - val server = Server(configuration) + val server = Server(configuration) - server.onMessage { testObject -> - send(testObject+1) - } - - server.bind() + server.onMessage { testObject -> + send(testObject+1) } + server + } - run { - val client = Client(clientConfig()) - client.onMessage { testObject -> - send(testObject+1) - } + val client = run { + val client = Client(clientConfig()) - client.onConnect { - send(0L) - } + client.onMessage { testObject -> + send(testObject+1) + } - client.connect() + client.onConnect { + send(0L) } - Thread.sleep(Long.MAX_VALUE) + client } - } - @Test - fun runForeverTestKryoPool() { - @Suppress("UNCHECKED_CAST") - val serialization = serverConfig().serialization as Serialization - - // 17 to force a pool size change - var kryo1: KryoExtra - var kryo2: KryoExtra - var kryo3: KryoExtra - var kryo4: KryoExtra - var kryo5: KryoExtra - var kryo6: KryoExtra - var kryo7: KryoExtra - var kryo8: KryoExtra - var kryo9: KryoExtra - var kryo10: KryoExtra - var kryo11: KryoExtra - var kryo12: KryoExtra - var kryo13: KryoExtra - var kryo14: KryoExtra - var kryo15: KryoExtra - var kryo16: KryoExtra - var kryo17: KryoExtra - - while (true) { - kryo1 = serialization.takeKryo() - kryo2 = serialization.takeKryo() - kryo3 = serialization.takeKryo() - kryo4 = serialization.takeKryo() - kryo5 = serialization.takeKryo() - kryo6 = serialization.takeKryo() - kryo7 = serialization.takeKryo() - kryo8 = serialization.takeKryo() - kryo9 = serialization.takeKryo() - kryo10 = serialization.takeKryo() - kryo11 = serialization.takeKryo() - kryo12 = serialization.takeKryo() - kryo13 = serialization.takeKryo() - kryo14 = serialization.takeKryo() - kryo15 = serialization.takeKryo() - kryo16 = serialization.takeKryo() - kryo17 = serialization.takeKryo() - - - serialization.returnKryo(kryo1) - serialization.returnKryo(kryo2) - serialization.returnKryo(kryo3) - serialization.returnKryo(kryo4) - serialization.returnKryo(kryo5) - serialization.returnKryo(kryo6) - serialization.returnKryo(kryo7) - serialization.returnKryo(kryo8) - serialization.returnKryo(kryo9) - serialization.returnKryo(kryo10) - serialization.returnKryo(kryo11) - serialization.returnKryo(kryo12) - serialization.returnKryo(kryo13) - serialization.returnKryo(kryo14) - serialization.returnKryo(kryo15) - serialization.returnKryo(kryo16) - serialization.returnKryo(kryo17) - } + server.bindIpc() + client.connectIpc() + + Thread.sleep(Long.MAX_VALUE) } private interface TestObject { diff --git a/test/dorkboxTest/network/rmi/RmiCommonTest.kt b/test/dorkboxTest/network/rmi/RmiCommonTest.kt index 33bc50f2..40dc1129 100644 --- a/test/dorkboxTest/network/rmi/RmiCommonTest.kt +++ b/test/dorkboxTest/network/rmi/RmiCommonTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,25 +12,6 @@ * 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. - * - * Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package dorkboxTest.network.rmi @@ -38,20 +19,171 @@ import dorkbox.network.connection.Connection import dorkbox.network.rmi.RemoteObject import dorkboxTest.network.rmi.cows.MessageWithTestCow import dorkboxTest.network.rmi.cows.TestCow +import kotlinx.coroutines.runBlocking import org.junit.Assert object RmiCommonTest { - suspend fun runTests(connection: Connection, test: TestCow, remoteObjectID: Int) { - val remoteObject = test as RemoteObject + fun runTimeoutTest(connection: Connection, test: TestCow) { + val remoteObject = RemoteObject.cast(test) + + remoteObject.responseTimeout = 1000 + try { + test.moo("A You should see this two seconds before...", 2000) + Assert.fail("We should be throwing a timeout exception!") + } catch (ignored: Exception) { + } + + try { + test.moo("B You should see this two seconds before...", 200) + } catch (ignored: Exception) { + Assert.fail("We should NOT be throwing a timeout exception!") + } + + runBlocking { + try { + test.mooSuspend("C You should see this two seconds before...", 2000) + Assert.fail("We should be throwing a timeout exception!") + } catch (ignored: Exception) { + } + + try { + test.mooSuspend("D You should see this two seconds before...", 200) + } catch (ignored: Exception) { + Assert.fail("We should NOT be throwing a timeout exception!") + } + } + + + // Test sending a reference to a remote object. + val m = MessageWithTestCow(test) + m.number = 678 + m.text = "sometext" + connection.send(m) + + remoteObject.enableHashCode(true) + remoteObject.enableEquals(true) + + connection.logger.error("Finished tests") + } + + fun runSyncTest(connection: Connection, test: TestCow) { + val remoteObject = RemoteObject.cast(test) + + remoteObject.responseTimeout = 1000 + + remoteObject.sync { + try { + test.moo("You should see this two seconds before...", 2000) + Assert.fail("We should be throwing a timeout exception!") + } catch (ignored: Exception) { + } + } + + runBlocking { + remoteObject.syncSuspend { + try { + test.mooSuspend("You should see this two seconds before...", 2000) + Assert.fail("We should be throwing a timeout exception!") + } catch (ignored: Exception) { + } + } + } + + + // Test sending a reference to a remote object. + val m = MessageWithTestCow(test) + m.number = 678 + m.text = "sometext" + connection.send(m) + + remoteObject.enableHashCode(true) + remoteObject.enableEquals(true) + + connection.logger.error("Finished tests") + } + + fun runASyncTest(connection: Connection, test: TestCow) { + val remoteObject = RemoteObject.cast(test) + + remoteObject.responseTimeout = 100 + remoteObject.async { + try { + test.moo("You should see this 400 m-seconds before...", 400) + } catch (ignored: Exception) { + Assert.fail("We should NOT be throwing a timeout exception!") + } + } + + + remoteObject.sync { + try { + test.moo("You should see this 400 m-seconds before...", 400) + Assert.fail("We should be throwing a timeout exception!") + } catch (ignored: Exception) { + } + + remoteObject.async { + remoteObject.sync { + try { + test.moo("You should see this 400 m-seconds before...", 400) + Assert.fail("We should be throwing a timeout exception!") + } catch (ignored: Exception) { + } + } + + try { + test.moo("You should see this 400 m-seconds before...", 400) + } catch (ignored: Exception) { + Assert.fail("We should NOT be throwing a timeout exception!") + } + } + + try { + test.moo("You should see this 400 m-seconds before...", 400) + Assert.fail("We should be throwing a timeout exception!") + } catch (ignored: Exception) { + } + } + + + + runBlocking { + remoteObject.asyncSuspend { + try { + test.mooSuspend("You should see this 400 m-seconds before...", 400) + } catch (ignored: Exception) { + Assert.fail("We should NOT be throwing a timeout exception!") + } + } + } + + + // Test sending a reference to a remote object. + val m = MessageWithTestCow(test) + m.number = 678 + m.text = "sometext" + connection.send(m) + + remoteObject.enableHashCode(true) + remoteObject.enableEquals(true) + + connection.logger.error("Finished tests") + } + + fun runTests(connection: Connection, test: TestCow, remoteObjectID: Int) { + val remoteObject = RemoteObject.cast(test) // Default behavior. RMI is transparent, method calls behave like normal // (return values and exceptions are returned, call is synchronous) connection.logger.error("hashCode: " + test.hashCode()) connection.logger.error("toString: $test") - test.withSuspend("test", 32) - val s1 = test.withSuspendAndReturn("test", 32) - Assert.assertEquals(s1, 32) + runBlocking { + test.withSuspend("test", 32) + val s1 = test.withSuspendAndReturn("test", 32) + Assert.assertEquals(s1, 32) + } + // see what the "remote" toString() method is @@ -69,6 +201,14 @@ object RmiCommonTest { connection.logger.error("...This") remoteObject.responseTimeout = 3000 + runBlocking { + remoteObject.responseTimeout = 5000 + test.mooSuspend("You should see this two seconds before...", 2000) + connection.logger.error("...This") + remoteObject.responseTimeout = 3000 + } + + // Try exception handling try { test.throwException() @@ -77,20 +217,47 @@ object RmiCommonTest { connection.logger.error("Expected exception (exception log should also be on the object impl side).", e) } - try { - test.throwSuspendException() - Assert.fail("sync should be throwing an exception!") - } catch (e: UnsupportedOperationException) { - connection.logger.error("\tExpected exception (exception log should also be on the object impl side).", e) + runBlocking { + try { + test.throwSuspendException() + Assert.fail("sync should be throwing an exception!") + } + catch (e: UnsupportedOperationException) { + connection.logger.error("\tExpected exception (exception log should also be on the object impl side).", e) + } + } + + + remoteObject.sync { + moo("Bzzzzzz") } + runBlocking { + remoteObject.syncSuspend { + moo("Bzzzzzz----MOOO", 22) + } + } + + // Non-blocking call tests // Non-blocking call tests // Non-blocking call tests connection.logger.error("I'm currently async: ${remoteObject.async}. Now testing ASYNC") - remoteObject.async = true + runBlocking { + remoteObject.asyncSuspend { + // calls that ignore the return value + mooSuspend("Bark. should wait 4 seconds", 4000) // this should not timeout (because it's async!) + + // Non-blocking call that ignores the return value + Assert.assertEquals(0, test.id().toLong()) + } + } + + + // default is false + remoteObject.async = true // calls that ignore the return value test.moo("Meow") @@ -108,11 +275,14 @@ object RmiCommonTest { Assert.fail("Async should not be throwing an exception!") } - try { - test.throwSuspendException() - } catch (e: IllegalStateException) { - // exceptions are not caught when async = true! - Assert.fail("Async should not be throwing an exception!") + runBlocking { + try { + test.throwSuspendException() + } + catch (e: IllegalStateException) { + // exceptions are not caught when async = true! + Assert.fail("Async should not be throwing an exception!") + } } @@ -122,13 +292,16 @@ object RmiCommonTest { test.moo("Mooooooooo", 4000) - // should wait for a small time + // should wait for a small amount of time remoteObject.async = false remoteObject.responseTimeout = 6000 connection.logger.error("You should see this 2 seconds before") - val slow = test.slow() - connection.logger.error("...This") - Assert.assertEquals(slow.toDouble(), 123.0, 0.0001) + + runBlocking { + val slow = test.slow() + connection.logger.error("...This") + Assert.assertEquals(slow.toDouble(), 123.0, 0.0001) + } // Test sending a reference to a remote object. @@ -137,6 +310,9 @@ object RmiCommonTest { m.text = "sometext" connection.send(m) + remoteObject.enableHashCode(true) + remoteObject.enableEquals(true) + connection.logger.error("Finished tests") } } diff --git a/test/dorkboxTest/network/rmi/RmiDelayedInvocationTest.kt b/test/dorkboxTest/network/rmi/RmiDelayedInvocationTest.kt index 628506de..21d3e0be 100644 --- a/test/dorkboxTest/network/rmi/RmiDelayedInvocationTest.kt +++ b/test/dorkboxTest/network/rmi/RmiDelayedInvocationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,8 @@ import dorkbox.network.Server import dorkbox.network.connection.Connection import dorkbox.network.serialization.Serialization import dorkboxTest.network.BaseTest -import kotlinx.coroutines.runBlocking import org.junit.Test +import java.util.concurrent.* import java.util.concurrent.atomic.* class RmiDelayedInvocationTest : BaseTest() { @@ -50,7 +50,8 @@ class RmiDelayedInvocationTest : BaseTest() { * uses the first remote object to get the second remote object. */ fun rmi(config: Configuration.() -> Unit = {}) { - run { + val countDownLatch = CountDownLatch(1) + val server = run { val configuration = serverConfig() config(configuration) register(configuration.serialization) @@ -58,11 +59,11 @@ class RmiDelayedInvocationTest : BaseTest() { val server = Server(configuration) addEndPoint(server) - server.rmiGlobal.save(TestObjectImpl(iterateLock), OBJ_ID) - server.bind() + server.rmiGlobal.save(TestObjectImpl(countDownLatch), OBJ_ID) + server } - run { + val client = run { val configuration = clientConfig() config(configuration) @@ -85,18 +86,16 @@ class RmiDelayedInvocationTest : BaseTest() { } // sometimes, this method is never called right away. + // this will also count-down the latch remoteObject.setOther(i.toFloat()) - runBlocking { - synchronized(iterateLock) { - try { - (iterateLock as Object).wait(1) - } catch (e: InterruptedException) { - logger.error("Failed after: $i") - e.printStackTrace() - abort = true - } - } + try { + // it should be instant!! + countDownLatch.await(10, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + logger.error("Failed after: $i") + e.printStackTrace() + abort = true } } logger.error("Done with delay invocation test") @@ -104,9 +103,12 @@ class RmiDelayedInvocationTest : BaseTest() { stopEndPoints() } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) + waitForThreads() } @@ -115,15 +117,14 @@ class RmiDelayedInvocationTest : BaseTest() { fun other(): Float } - private class TestObjectImpl(private val iterateLock: Any) : TestObject { + private class TestObjectImpl(private val latch: CountDownLatch) : TestObject { @Transient private val ID = idCounter.getAndIncrement() private var aFloat = 0f override fun setOther(aFloat: Float) { this.aFloat = aFloat - - synchronized(iterateLock) { (iterateLock as Object).notify() } + latch.countDown() } override fun other(): Float { diff --git a/test/dorkboxTest/network/rmi/RmiDuplicateObjectTest.kt b/test/dorkboxTest/network/rmi/RmiDuplicateObjectTest.kt index 6ed19342..2b9268ea 100644 --- a/test/dorkboxTest/network/rmi/RmiDuplicateObjectTest.kt +++ b/test/dorkboxTest/network/rmi/RmiDuplicateObjectTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,7 +12,8 @@ * 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. - * + */ +/* * Copyright (c) 2008, Nathan Sweet * All rights reserved. * @@ -34,7 +35,6 @@ */ package dorkboxTest.network.rmi -import ch.qos.logback.classic.Level import dorkbox.netUtil.IPv4 import dorkbox.netUtil.IPv6 import dorkbox.network.Client @@ -47,6 +47,7 @@ import dorkboxTest.network.rmi.cows.TestCow import dorkboxTest.network.rmi.cows.TestCowImpl import org.junit.Assert import org.junit.Test +import java.util.concurrent.* class RmiDuplicateObjectTest : BaseTest() { @Test @@ -79,20 +80,19 @@ class RmiDuplicateObjectTest : BaseTest() { private fun doConnect(isIpv4: Boolean, isIpv6: Boolean, runIpv4Connect: Boolean, client: Client) { when { - isIpv4 && isIpv6 && runIpv4Connect -> client.connect(IPv4.LOCALHOST) - isIpv4 && isIpv6 && !runIpv4Connect -> client.connect(IPv6.LOCALHOST) - isIpv4 -> client.connect(IPv4.LOCALHOST) - isIpv6 -> client.connect(IPv6.LOCALHOST) - else -> client.connect() + isIpv4 && isIpv6 && runIpv4Connect -> client.connect(IPv4.LOCALHOST, 2000) + isIpv4 && isIpv6 && !runIpv4Connect -> client.connect(IPv6.LOCALHOST, 2000) + isIpv4 -> client.connect(IPv4.LOCALHOST, 2000) + isIpv6 -> client.connect(IPv6.LOCALHOST, 2000) + else -> client.connect(IPv4.LOCALHOST, 2000) } } - private val objs = mutableSetOf() - fun rmi(isIpv4: Boolean = false, isIpv6: Boolean = false, runIpv4Connect: Boolean = true, config: Configuration.() -> Unit = {}) { - setLogLevel(Level.TRACE) + val objs = mutableSetOf() + val latch = CountDownLatch(2) - run { + val server = run { val configuration = serverConfig() configuration.enableIPv4 = isIpv4 configuration.enableIPv6 = isIpv6 @@ -105,21 +105,24 @@ class RmiDuplicateObjectTest : BaseTest() { val server = Server(configuration) addEndPoint(server) - server.bind() + server.onConnect { - server.forEachConnection { - val testCow = it.rmi.get(4) - testCow.moo() + logger.warn("Starting to moo") + // these are on separate threads (client.init) and this -- there can be race conditions, where the object doesn't exist yet! + val testCow = rmi.get(4) + testCow.moo() - synchronized(objs) { - objs.add(testCow.id()) - } + synchronized(objs) { + objs.add(testCow.id()) } + latch.countDown() } + + server } - run { + val client1 = run { val configuration = clientConfig() config(configuration) @@ -127,12 +130,14 @@ class RmiDuplicateObjectTest : BaseTest() { addEndPoint(client) client.onInit { + logger.warn("Initializing moo 4") rmi.save(TestCowImpl(4), 4) } - doConnect(isIpv4, isIpv6, runIpv4Connect, client) + client } - run { + + val client2 = run { val configuration = clientConfig() config(configuration) @@ -141,14 +146,21 @@ class RmiDuplicateObjectTest : BaseTest() { client.onInit { - rmi.save(TestCowImpl(5), 4) + logger.warn("Initializing moo 5") + rmi.save(TestCowImpl(5), 4) // both are saved as ID 4 (but internally are 4 and 5) } - doConnect(isIpv4, isIpv6, runIpv4Connect, client) + client } - waitForThreads(5) + server.bind(2000) + doConnect(isIpv4, isIpv6, runIpv4Connect, client1) + doConnect(isIpv4, isIpv6, runIpv4Connect, client2) + + latch.await() + stopEndPoints() + waitForThreads() val actual = synchronized(objs) { objs.joinToString() diff --git a/test/dorkboxTest/network/rmi/RmiNestedSuspendTest.kt b/test/dorkboxTest/network/rmi/RmiNestedSuspendTest.kt new file mode 100644 index 00000000..c852876d --- /dev/null +++ b/test/dorkboxTest/network/rmi/RmiNestedSuspendTest.kt @@ -0,0 +1,459 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network.rmi + +import dorkbox.network.Client +import dorkbox.network.Server +import dorkbox.network.connection.Connection +import dorkboxTest.network.BaseTest +import kotlinx.coroutines.runBlocking +import org.junit.Assert +import org.junit.Test +import java.util.concurrent.atomic.* + + + +class RmiNestedSuspendTest : BaseTest() { + companion object { + private val idCounter = AtomicInteger() + } + + /** + * In this test the server has two objects in an object space. + * + * The client uses the first remote object to get the second remote object. + * + * + * The MAJOR difference in this version, is that we use an interface to override the methods, so that we can have the RMI system pass + * in the connection object. + * + * Specifically, from CachedMethod.java + * + * In situations where we want to pass in the Connection (to an RMI method), we have to be able to override method A, with method B. + * This is to support calling RMI methods from an interface (that does pass the connection reference) to + * an implType, that DOES pass the connection reference. The remote side (that initiates the RMI calls), MUST use + * the interface, and the implType may override the method, so that we add the connection as the first in + * the list of parameters. + * + * for example: + * Interface: foo(String x) + * Impl: foo(Connection c, String x) + * + * The implType (if it exists, with the same name, and with the same signature + connection parameter) will be called from the interface + * instead of the method that would NORMALLY be called. + */ + @Test + fun biDirectionalDoubleRmi() { + val server = run { + val configuration = serverConfig() + + configuration.serialization.rmi.register(TestObject::class.java, TestObjectAnnotImpl::class.java) + configuration.serialization.rmi.register(OtherObject::class.java, OtherObjectImpl::class.java) + + val server = Server(configuration) + addEndPoint(server) + + server.onMessage { message -> + // The test is complete when the client sends the OtherObject instance. + // this 'object' is the REAL object, not a proxy, because this object is created within this connection. + if (message.value() == 12.34f) { + stopEndPoints() + } else { + Assert.fail("Incorrect object value") + } + } + + server + } + + + val client = run { + val configuration = clientConfig() + + val client = Client(configuration) + addEndPoint(client) + + client.onConnect { + logger.error("Connected") + + rmi.create { + logger.error("Starting test") + runBlocking { + setValue(43.21f) + + // Normal remote method call. + Assert.assertEquals(43.21f, other(), .0001f) + + // Make a remote method call that returns another remote proxy object. + // the "test" object exists in the REMOTE side, as does the "OtherObject" that is created. + // here we have a proxy to both of them. + val otherObject: OtherObject = getOtherObject() + + // Normal remote method call on the second object. + otherObject.setValue(12.34f) + val value = otherObject.value() + Assert.assertEquals(12.34f, value, .0001f) + + + // make sure the "local" object and the "remote" object have the same values + Assert.assertEquals(12.34f, getOtherValue(), .0001f) + + // When a proxy object is sent, the other side receives its ACTUAL object (not a proxy of it), because + // that is where that object actually exists. + send(otherObject) + } + } + } + + client + } + + server.bind(2000) + client.connect(LOCALHOST, 2000) + + waitForThreads() + } + + @Test + fun doubleRmi() { + val server = run { + val configuration = serverConfig() + configuration.serialization.rmi.register(TestObject::class.java, TestObjectAnnotImpl::class.java) + configuration.serialization.rmi.register(OtherObject::class.java, OtherObjectImpl::class.java) + + val server = Server(configuration) + addEndPoint(server) + + server.onMessage { message -> + // The test is complete when the client sends the OtherObject instance. + // this 'object' is the REAL object, not a proxy, because this object is created within this connection. + if (message.value() == 12.34f) { + stopEndPoints() + } else { + Assert.fail("Incorrect object value") + } + } + + server + } + + + val client = run { + val configuration = clientConfig() + + val client = Client(configuration) + addEndPoint(client) + + client.onConnect { + logger.error("Connected") + rmi.create { + logger.error("Starting test") + runBlocking { + setValue(43.21f) + + // Normal remote method call. + Assert.assertEquals(43.21f, other(), .0001f) + + // Make a remote method call that returns another remote proxy object. + // the "test" object exists in the REMOTE side, as does the "OtherObject" that is created. + // here we have a proxy to both of them. + val otherObject: OtherObject = getOtherObject() + + // Normal remote method call on the second object. + otherObject.setValue(12.34f) + val value = otherObject.value() + Assert.assertEquals(12.34f, value, .0001f) + + + // make sure the "local" object and the "remote" object have the same values + Assert.assertEquals(12.34f, getOtherValue(), .0001f) + + // When a proxy object is sent, the other side receives its ACTUAL object (not a proxy of it), because + // that is where that object actually exists. + send(otherObject) + } + } + } + + client + } + + server.bind(2000) + client.connect(LOCALHOST, 2000) + waitForThreads() + } + + @Test + fun singleRmi() { + val server = run { + val configuration = serverConfig() + configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) + configuration.serialization.register(OtherObjectImpl::class.java) + + val server = Server(configuration) + addEndPoint(server) + + server.onMessage { message -> + // The test is complete when the client sends the OtherObject instance. + // this 'object' is the REAL object + if (message.value() == 43.21f) { + stopEndPoints() + } else { + Assert.fail("Incorrect object value") + } + } + + server + } + + + val client = run { + val configuration = clientConfig() + + val client = Client(configuration) + addEndPoint(client) + + client.onConnect { + logger.error("Connected") + + rmi.create { + logger.error("Starting test") + runBlocking { + setOtherValue(43.21f) + + // Normal remote method call. + Assert.assertEquals(43.21f, getOtherValue(), .0001f) + } + + // real object + val otherObject: OtherObject = getOtherObject() + + // Normal remote method call on the second object. + val value = otherObject.value() + Assert.assertEquals(43.21f, value, .0001f) + + + // When a proxy object is sent, the other side receives its ACTUAL object (not a proxy of it), because + // that is where that object actually exists. + send(otherObject) + } + } + + client + } + + server.bind(2000) + client.connect(LOCALHOST, 2000) + + waitForThreads() + } + + @Test + fun singleReverseRmi() { + val server = run { + val configuration = serverConfig() + configuration.serialization.rmi.register(TestObject::class.java, null) + configuration.serialization.register(OtherObjectImpl::class.java) + + val server = Server(configuration) + addEndPoint(server) + + server.onConnect { + logger.error("Connected") + + rmi.create { + logger.error("Starting test") + runBlocking { + setOtherValue(43.21f) + + // Normal remote method call. + Assert.assertEquals(43.21f, getOtherValue(), .0001f) + } + + // real object + val otherObject: OtherObject = getOtherObject() + + // Normal remote method call on the second object. + val value = otherObject.value() + Assert.assertEquals(43.21f, value, .0001f) + + + // When a proxy object is sent, the other side receives its ACTUAL object (not a proxy of it), because + // that is where that object actually exists. + send(otherObject) + } + } + + server + } + + + val client = run { + val configuration = clientConfig() + configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) + + val client = Client(configuration) + addEndPoint(client) + + client.onMessage { message -> + // The test is complete when the client sends the OtherObject instance. + // this 'object' is the REAL object + if (message.value() == 43.21f) { + stopEndPoints() + } else { + Assert.fail("Incorrect object value") + } + } + + client + } + + server.bind(2000) + client.connect(LOCALHOST, 2000) + + waitForThreads() + } + + + + + + private interface TestObject { + suspend fun setValue(aFloat: Float) + suspend fun setOtherValue(aFloat: Float) + suspend fun getOtherValue(): Float + fun other(): Float + fun getOtherObject(): OtherObject + } + + private interface OtherObject { + fun setValue(aFloat: Float) + fun value(): Float + } + + private class TestObjectImpl : TestObject { + @Transient + private val ID = idCounter.getAndIncrement() + + private val otherObject: OtherObject = OtherObjectImpl() + + private var aFloat = 0f + override suspend fun setValue(aFloat: Float) { + throw RuntimeException("Whoops!") + } + + suspend fun setValue(connection: Connection, aFloat: Float) { + connection.logger.error("receiving") + this.aFloat = aFloat + } + + override suspend fun setOtherValue(aFloat: Float) { + otherObject.setValue(aFloat) + } + + override suspend fun getOtherValue(): Float { + return otherObject.value() + } + + override fun other(): Float { + throw RuntimeException("Whoops!") + } + + @Suppress("UNUSED_PARAMETER") + fun other(connection: Connection): Float { + return aFloat + } + + override fun getOtherObject(): OtherObject { + return otherObject + } + + override fun hashCode(): Int { + return ID + } + } + + private class TestObjectAnnotImpl : TestObject { + @Transient + private val id = idCounter.getAndIncrement() + + private val otherObject: OtherObject = OtherObjectImpl() + + private var aFloat = 0f + override suspend fun setValue(aFloat: Float) { + throw RuntimeException("Whoops!") + } + + suspend fun setValue(connection: Connection, aFloat: Float) { + connection.logger.error("receiving") + this.aFloat = aFloat + } + + override suspend fun setOtherValue(aFloat: Float) { + otherObject.setValue(aFloat) + } + + override suspend fun getOtherValue(): Float { + return otherObject.value() + } + + override fun other(): Float { + throw RuntimeException("Whoops!") + } + + @Suppress("UNUSED_PARAMETER") + fun other(connection: Connection): Float { + return aFloat + } + + override fun getOtherObject(): OtherObject { + return otherObject + } + + override fun hashCode(): Int { + return id + } + } + + class OtherObjectImpl : OtherObject { + @Transient + private val ID = idCounter.getAndIncrement() + private var aFloat = 0f + override fun setValue(aFloat: Float) { + this.aFloat = aFloat + } + + override fun value(): Float { + return aFloat + } + + override fun hashCode(): Int { + return ID + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OtherObjectImpl + + if (ID != other.ID) return false + if (aFloat != other.aFloat) return false + + return true + } + } +} diff --git a/test/dorkboxTest/network/rmi/RmiNestedTest.kt b/test/dorkboxTest/network/rmi/RmiNestedTest.kt index 4e2faf47..c2c2cdb1 100644 --- a/test/dorkboxTest/network/rmi/RmiNestedTest.kt +++ b/test/dorkboxTest/network/rmi/RmiNestedTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,7 +56,7 @@ class RmiNestedTest : BaseTest() { */ @Test fun biDirectionalDoubleRmi() { - run { + val server = run { val configuration = serverConfig() configuration.serialization.rmi.register(TestObject::class.java, TestObjectAnnotImpl::class.java) @@ -75,11 +75,11 @@ class RmiNestedTest : BaseTest() { } } - server.bind() + server } - run { + val client = run { val configuration = clientConfig() val client = Client(configuration) @@ -116,14 +116,18 @@ class RmiNestedTest : BaseTest() { } } - client.connect(LOCALHOST) + client } + + server.bind(2000) + client.connect(LOCALHOST, 2000) + waitForThreads() } @Test fun doubleRmi() { - run { + val server = run { val configuration = serverConfig() configuration.serialization.rmi.register(TestObject::class.java, TestObjectAnnotImpl::class.java) configuration.serialization.rmi.register(OtherObject::class.java, OtherObjectImpl::class.java) @@ -141,11 +145,11 @@ class RmiNestedTest : BaseTest() { } } - server.bind() + server } - run { + val client = run { val configuration = clientConfig() val client = Client(configuration) @@ -181,14 +185,17 @@ class RmiNestedTest : BaseTest() { } } - client.connect(LOCALHOST) + client } + + server.bind(2000) + client.connect(LOCALHOST, 2000) waitForThreads() } @Test fun singleRmi() { - run { + val server = run { val configuration = serverConfig() configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) configuration.serialization.register(OtherObjectImpl::class.java) @@ -206,11 +213,11 @@ class RmiNestedTest : BaseTest() { } } - server.bind() + server } - run { + val client = run { val configuration = clientConfig() val client = Client(configuration) @@ -240,14 +247,18 @@ class RmiNestedTest : BaseTest() { } } - client.connect(LOCALHOST) + client } + + server.bind(2000) + client.connect(LOCALHOST, 2000) + waitForThreads() } @Test fun singleReverseRmi() { - run { + val server = run { val configuration = serverConfig() configuration.serialization.rmi.register(TestObject::class.java, null) configuration.serialization.register(OtherObjectImpl::class.java) @@ -279,11 +290,11 @@ class RmiNestedTest : BaseTest() { } } - server.bind() + server } - run { + val client = run { val configuration = clientConfig() configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) @@ -300,8 +311,12 @@ class RmiNestedTest : BaseTest() { } } - client.connect(LOCALHOST) + client } + + server.bind(2000) + client.connect(LOCALHOST, 2000) + waitForThreads() } @@ -310,9 +325,9 @@ class RmiNestedTest : BaseTest() { private interface TestObject { - suspend fun setValue(aFloat: Float) - suspend fun setOtherValue(aFloat: Float) - suspend fun getOtherValue(): Float + fun setValue(aFloat: Float) + fun setOtherValue(aFloat: Float) + fun getOtherValue(): Float fun other(): Float fun getOtherObject(): OtherObject } @@ -329,20 +344,20 @@ class RmiNestedTest : BaseTest() { private val otherObject: OtherObject = OtherObjectImpl() private var aFloat = 0f - override suspend fun setValue(aFloat: Float) { + override fun setValue(aFloat: Float) { throw RuntimeException("Whoops!") } - suspend fun setValue(connection: Connection, aFloat: Float) { + fun setValue(connection: Connection, aFloat: Float) { connection.logger.error("receiving") this.aFloat = aFloat } - override suspend fun setOtherValue(aFloat: Float) { + override fun setOtherValue(aFloat: Float) { otherObject.setValue(aFloat) } - override suspend fun getOtherValue(): Float { + override fun getOtherValue(): Float { return otherObject.value() } @@ -371,20 +386,20 @@ class RmiNestedTest : BaseTest() { private val otherObject: OtherObject = OtherObjectImpl() private var aFloat = 0f - override suspend fun setValue(aFloat: Float) { - throw RuntimeException("Whoops!") + override fun setValue(aFloat: Float) { + throw RuntimeException("Whoops!") // the connection param version should be called. } - suspend fun setValue(connection: Connection, aFloat: Float) { + fun setValue(connection: Connection, aFloat: Float) { connection.logger.error("receiving") this.aFloat = aFloat } - override suspend fun setOtherValue(aFloat: Float) { + override fun setOtherValue(aFloat: Float) { otherObject.setValue(aFloat) } - override suspend fun getOtherValue(): Float { + override fun getOtherValue(): Float { return otherObject.value() } diff --git a/test/dorkboxTest/network/rmi/RmiResponseManagerTest.kt b/test/dorkboxTest/network/rmi/RmiResponseManagerTest.kt new file mode 100644 index 00000000..24298dda --- /dev/null +++ b/test/dorkboxTest/network/rmi/RmiResponseManagerTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network.rmi + +import dorkbox.network.rmi.ResponseManager +import dorkboxTest.network.BaseTest +import kotlinx.atomicfu.AtomicInt +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.actor +import org.junit.Assert +import org.junit.Test +import org.slf4j.LoggerFactory + +class RmiResponseManagerTest: BaseTest() { + companion object { + private val logger = LoggerFactory.getLogger("RmiResponseManagerTest") + } + + @Test + fun rmiResponseRoundTrip() { + runTest(2, 3) + runTest(2, 30) + runTest(2, 300) + runTest(2, 3000) + runTest(2, 65535) + + runTest(65535 * 2, 3) + runTest(65535 * 2, 30) + runTest(65535 * 2, 300) + runTest(65535 * 2, 3000) + runTest(65535 * 2, 65535) + } + + @Test + fun rmiResponseInvalidRoundTrip() { + runTest(65535 * 2, 65535) + runTest(65535 * 2, 1, false) + runTest(65535 * 2, 65538, false) + } + + @OptIn(ObsoleteCoroutinesApi::class) + private fun runTest(totalCount: Int, responseMangerSize: Int, expectedToPass: Boolean = true) { + val counted: AtomicInt = atomic(totalCount) + + try { + val responseManager = ResponseManager(responseMangerSize) + + runBlocking { + val actor = actor>(Dispatchers.Default, 0) { + for (e in this) { + val await = e.await() + val waiterCallback = responseManager.removeWaiterCallback<() -> Unit>(await, logger) + Assert.assertTrue(waiterCallback != null) + + waiterCallback!!.invoke() + } + } + + repeat(totalCount) { + async { + responseManager.prepWithCallback(logger) { + // logger.error { "function invoked $it!" } + counted.decrementAndGet() + } + }.also { + actor.send(it) + } + } + + actor.close() + // data is fully processed once the runBlocking is exited. Before then, it is not. (the "capacity" of the actor is "always there") + } + } catch (e: Exception) { + if (expectedToPass) { + throw e + } else { + Assert.assertTrue(true) + } + } + + if (expectedToPass) { + Assert.assertEquals(0, counted.value) + } else { + Assert.assertTrue(true) + } + } +} diff --git a/test/dorkboxTest/network/rmi/RmiSimpleActionsTest.kt b/test/dorkboxTest/network/rmi/RmiSimpleActionsTest.kt index f9aef22b..b20edbb8 100644 --- a/test/dorkboxTest/network/rmi/RmiSimpleActionsTest.kt +++ b/test/dorkboxTest/network/rmi/RmiSimpleActionsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,25 +12,6 @@ * 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. - * - * Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package dorkboxTest.network.rmi @@ -50,7 +31,6 @@ import org.junit.Test class RmiSimpleActionsTest : BaseTest() { @Test fun testGlobalDelete() { - val configuration = serverConfig() configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java) configuration.serialization.register(MessageWithTestCow::class.java) @@ -82,6 +62,9 @@ class RmiSimpleActionsTest : BaseTest() { Assert.assertTrue(server.rmiGlobal.delete(testCowImpl)) Assert.assertFalse(server.rmiGlobal.delete(testCowImpl)) Assert.assertFalse(server.rmiGlobal.delete(newId2)) + + stopEndPoints() + waitForThreads() } @Test @@ -91,16 +74,18 @@ class RmiSimpleActionsTest : BaseTest() { private fun doConnect(isIpv4: Boolean, isIpv6: Boolean, runIpv4Connect: Boolean, client: Client) { when { - isIpv4 && isIpv6 && runIpv4Connect -> client.connect(IPv4.LOCALHOST) - isIpv4 && isIpv6 && !runIpv4Connect -> client.connect(IPv6.LOCALHOST) - isIpv4 -> client.connect(IPv4.LOCALHOST) - isIpv6 -> client.connect(IPv6.LOCALHOST) - else -> client.connect() + isIpv4 && isIpv6 && runIpv4Connect -> client.connect(IPv4.LOCALHOST, 2000) + isIpv4 && isIpv6 && !runIpv4Connect -> client.connect(IPv6.LOCALHOST, 2000) + isIpv4 -> client.connect(IPv4.LOCALHOST, 2000) + isIpv6 -> client.connect(IPv6.LOCALHOST, 2000) + else -> client.connectIpc() } } fun rmiConnectionDelete(isIpv4: Boolean = false, isIpv6: Boolean = false, runIpv4Connect: Boolean = true, config: Configuration.() -> Unit = {}) { - run { + var RMI_ID = 0 + + val server = run { val configuration = serverConfig() configuration.enableIPv4 = isIpv4 configuration.enableIPv6 = isIpv6 @@ -115,7 +100,6 @@ class RmiSimpleActionsTest : BaseTest() { val server = Server(configuration) addEndPoint(server) - server.bind() server.onMessage { m -> server.logger.error("Received finish signal for test for: Client -> Server") @@ -127,14 +111,20 @@ class RmiSimpleActionsTest : BaseTest() { server.logger.error("Finished test for: Client -> Server") - rmi.delete(23) - // `object` is still a reference to the object! - // so we don't want to pass that back -- so pass back a new one - send(MessageWithTestCow(TestCowImpl(1))) + rmi.delete(RMI_ID) + + val newID = RMI_ID+123 + val testCow = TestCowImpl(newID) + // we must manually save the object -- because if we don't we'll auto-create it when it gets sent across the network. + // this happens BECAUSE `TestCow` is an RMI object!! + rmi.save(testCow, newID) + send(MessageWithTestCow(testCow)) } + + server } - run { + val client = run { val configuration = clientConfig() config(configuration) // configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java) @@ -142,17 +132,25 @@ class RmiSimpleActionsTest : BaseTest() { val client = Client(configuration) addEndPoint(client) + + client.onConnect { rmi.create(23) { + RMI_ID = it client.logger.error("Running test for: Client -> Server") - RmiCommonTest.runTests(this@onConnect, this, 23) +// RmiCommonTest.runTests(this@onConnect, this@create, 23) + val m = MessageWithTestCow(this) + m.number = 678 + m.text = "sometext" + this@onConnect.send(m) + client.logger.error("Done with test for: Client -> Server") } } client.onMessage { _ -> // check if 23 still exists (it should not) - val obj = rmi.get(23) + val obj = rmi.get(RMI_ID) try { obj.id() @@ -161,12 +159,108 @@ class RmiSimpleActionsTest : BaseTest() { // this is expected } - stopEndPoints(2000) + stopEndPoints() } - doConnect(isIpv4, isIpv6, runIpv4Connect, client) + client } + server.bind(2000) + doConnect(isIpv4, isIpv6, runIpv4Connect, client) + + waitForThreads() + } + + @Test + fun rmiReconnectPersistence() { + var RMI_ID = 0 + + val server = run { + val configuration = serverConfig() + + configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java) + configuration.serialization.register(MessageWithTestCow::class.java) + configuration.serialization.register(UnsupportedOperationException::class.java) + + // for Client -> Server RMI + configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java) + + val server = Server(configuration) + addEndPoint(server) + + server.onMessage { m -> + server.logger.error("Received finish signal for test for: Client -> Server") + + val `object` = m.testCow + val id = `object`.id() + + Assert.assertEquals(23, id) + + server.logger.error("Finished test for: Client -> Server") + + rmi.delete(RMI_ID) + + val newID = RMI_ID+123 + val testCow = TestCowImpl(newID) + // we must manually save the object -- because if we don't we'll auto-create it when it gets sent across the network. + // this happens BECAUSE `TestCow` is an RMI object!! + rmi.save(testCow, newID) + send(MessageWithTestCow(testCow)) + } + + server + } + + val client = run { + var firstRun = true + val configuration = clientConfig() + // configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java) + + val client = Client(configuration) + addEndPoint(client) + + + client.onConnect { + rmi.create(23) { + RMI_ID = it + client.logger.error("Running test for: Client -> Server") +// RmiCommonTest.runTests(this@onConnect, this@create, 23) + + val m = MessageWithTestCow(this) + m.number = 678 + m.text = "sometext" + this@onConnect.send(m) + + client.logger.error("Done with test for: Client -> Server") + } + } + + client.onMessage { _ -> + // check if 23 still exists (it should not) + val obj = rmi.get(RMI_ID) + + try { + obj.id() + Assert.fail(".id() should throw a timeout/exception, the backing RMI object doesn't exist!") + } catch (e: Exception) { + // this is expected + } + + if (firstRun) { + firstRun = false + client.close(false) + client.connectIpc() + } else { + stopEndPoints() + } + } + + client + } + + server.bindIpc() + client.connectIpc() + waitForThreads() } } diff --git a/test/dorkboxTest/network/rmi/RmiSimpleTest.kt b/test/dorkboxTest/network/rmi/RmiSimpleTest.kt index 167a9e7d..dc6b0e82 100644 --- a/test/dorkboxTest/network/rmi/RmiSimpleTest.kt +++ b/test/dorkboxTest/network/rmi/RmiSimpleTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,7 +12,8 @@ * 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. - * + */ +/* * Copyright (c) 2008, Nathan Sweet * All rights reserved. * @@ -34,11 +35,9 @@ */ package dorkboxTest.network.rmi -import ch.qos.logback.classic.Level import dorkbox.netUtil.IPv4 import dorkbox.netUtil.IPv6 import dorkbox.network.Client -import dorkbox.network.Configuration import dorkbox.network.Server import dorkbox.network.connection.Connection import dorkboxTest.network.BaseTest @@ -51,33 +50,65 @@ import org.junit.Test class RmiSimpleTest : BaseTest() { + enum class ConnectType(val ip4: Boolean, val ip6: Boolean, val ipc: Boolean) { + IPC(false, false, true), + IPC4(true, false, true), + IPC6(false, true, true), + IPC46(true, true, true), + IPC64(true, true, true), + IP4(true, false, false), + IP6(false, true, false), + IP46(true, true, false), + IP64(true, true, false) + } + + @Test fun rmiIPv4NetworkGlobal() { - rmiGlobal(isIpv4 = true, isIpv6 = false) + rmiGlobal(ConnectType.IP4) } @Test fun rmiIPv6NetworkGlobal() { - rmiGlobal(isIpv4 = true, isIpv6 = false) + rmiGlobal(ConnectType.IP6) } @Test fun rmiBothIPv4ConnectNetworkGlobal() { - rmiGlobal(isIpv4 = true, isIpv6 = true) + rmiGlobal(ConnectType.IP46) } @Test fun rmiBothIPv6ConnectNetworkGlobal() { - rmiGlobal(isIpv4 = true, isIpv6 = true, runIpv4Connect = true) + rmiGlobal(ConnectType.IP64) } @Test fun rmiIpcNetworkGlobal() { - rmiGlobal { - enableIpc = true - } + rmiGlobal(ConnectType.IPC) } + @Test + fun rmiIpcNetworkGlobalFallback4() { + rmiGlobal(ConnectType.IPC4) + } + + @Test + fun rmiIpcNetworkGlobalFallback6() { + rmiGlobal(ConnectType.IPC6) + } + + @Test + fun rmiIpcNetworkGlobalFallback46() { + rmiGlobal(ConnectType.IPC46) + } + + @Test + fun rmiIpcNetworkGlobalFallback64() { + rmiGlobal(ConnectType.IPC64) + } + + @@ -85,50 +116,57 @@ class RmiSimpleTest : BaseTest() { @Test fun rmiIPv4NetworkConnection() { - rmi(isIpv4 = true, isIpv6 = false) + rmi(ConnectType.IP4) } @Test fun rmiIPv6NetworkConnection() { - rmi(isIpv4 = false, isIpv6 = true) + rmi(ConnectType.IP6) } @Test fun rmiBothIPv4ConnectNetworkConnection() { - rmi(isIpv4 = true, isIpv6 = true) + rmi(ConnectType.IP46) } @Test fun rmiBothIPv6ConnectNetworkConnection() { - rmi(isIpv4 = true, isIpv6 = true, runIpv4Connect = true) + rmi(ConnectType.IP64) } @Test fun rmiIpcNetworkConnection() { - rmi { - enableIpc = true - } + rmi(ConnectType.IPC) } - private fun doConnect(isIpv4: Boolean, isIpv6: Boolean, runIpv4Connect: Boolean, client: Client) { - when { - isIpv4 && isIpv6 && runIpv4Connect -> client.connect(IPv4.LOCALHOST) - isIpv4 && isIpv6 && !runIpv4Connect -> client.connect(IPv6.LOCALHOST) - isIpv4 -> client.connect(IPv4.LOCALHOST) - isIpv6 -> client.connect(IPv6.LOCALHOST) - else -> client.connect() - } + @Test + fun rmiIpcFallback4NetworkConnection() { + rmi(ConnectType.IPC4) } + @Test + fun rmiIpcFallback6NetworkConnection() { + rmi(ConnectType.IPC6) + } + + @Test + fun rmiIpcFallback46NetworkConnection() { + rmi(ConnectType.IPC46) + } + + @Test + fun rmiIpcFallback64NetworkConnection() { + rmi(ConnectType.IPC64) + } // GLOBAL rmi stuff cannot CREATE or DELETE (only save/get) - private fun rmiGlobal(isIpv4: Boolean = false, isIpv6: Boolean = false, runIpv4Connect: Boolean = true, config: Configuration.() -> Unit = {}) { - run { + private fun rmiGlobal(clientType: ConnectType, serverType: ConnectType = clientType) { + val server = run { val configuration = serverConfig() - configuration.enableIPv4 = isIpv4 - configuration.enableIPv6 = isIpv6 - config(configuration) + configuration.enableIPv4 = serverType.ip4 + configuration.enableIPv6 = serverType.ip6 + configuration.enableIpc = serverType.ipc configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java) configuration.serialization.register(MessageWithTestCow::class.java) @@ -139,7 +177,6 @@ class RmiSimpleTest : BaseTest() { val server = Server(configuration) addEndPoint(server) - server.bind() server.rmiGlobal.save(TestCowImpl(44), 44) @@ -159,22 +196,28 @@ class RmiSimpleTest : BaseTest() { RmiCommonTest.runTests(this@onMessage, rmi.get(4), 4) server.logger.error("Done with test for: Server -> Client") } + + server } - run { + val client = run { val configuration = clientConfig() - config(configuration) + configuration.enableIPv4 = clientType.ip4 + configuration.enableIPv6 = clientType.ip6 + configuration.enableIpc = clientType.ipc // configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java) val client = Client(configuration) addEndPoint(client) client.onConnect { - rmi.save(TestCowImpl(4), 4) + runBlocking { + rmi.save(TestCowImpl(4), 4) - client.logger.error("Running test for: Client -> Server") - RmiCommonTest.runTests(this, rmi.getGlobal(44), 44) - client.logger.error("Done with test for: Client -> Server") + client.logger.error("Running test for: Client -> Server") + RmiCommonTest.runTests(this@onConnect, rmi.getGlobal(44), 44) + client.logger.error("Done with test for: Client -> Server") + } } client.onMessage { m -> @@ -183,32 +226,141 @@ class RmiSimpleTest : BaseTest() { val id = `object`.id() Assert.assertEquals(4, id) client.logger.error("Finished test for: Client -> Server") - stopEndPoints(2000) + stopEndPoints() } - doConnect(isIpv4, isIpv6, runIpv4Connect, client) - client.logger.error("Starting test for: Client -> Server") - // this creates a GLOBAL object on the server (instead of a connection specific object) - runBlocking { + client + } + + server.bind(2000) + when (clientType) { + ConnectType.IPC -> client.connectIpc() + ConnectType.IPC4 -> client.connect(IPv4.LOCALHOST, 2000) + ConnectType.IPC6 -> client.connect(IPv6.LOCALHOST, 2000) + ConnectType.IPC46 -> client.connect(IPv4.LOCALHOST, 2000) + ConnectType.IPC64 -> client.connect(IPv6.LOCALHOST, 2000) + ConnectType.IP4 -> client.connect(IPv4.LOCALHOST, 2000) + ConnectType.IP6 -> client.connect(IPv6.LOCALHOST, 2000) + ConnectType.IP46 -> client.connect(IPv4.LOCALHOST, 2000) + ConnectType.IP64 -> client.connect(IPv6.LOCALHOST, 2000) + } + + waitForThreads() + } + + + fun rmi(clientType: ConnectType, serverType: ConnectType = clientType) { + val server = run { + val configuration = serverConfig() + configuration.enableIPv4 = serverType.ip4 + configuration.enableIPv6 = serverType.ip6 + configuration.enableIpc = serverType.ipc + + configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java) + configuration.serialization.register(MessageWithTestCow::class.java) + configuration.serialization.register(UnsupportedOperationException::class.java) + + + val server = Server(configuration) + addEndPoint(server) + + + server.onMessage { m -> + server.logger.error("Received finish signal for test for: Client -> Server") + val `object` = m.testCow + val id = `object`.id() + Assert.assertEquals(23, id) + server.logger.error("Finished test for: Client -> Server") + + + server.logger.error("Starting test for: Server -> Client") + // NOTE: THIS IS BI-DIRECTIONAL! + rmi.create(123) { + server.logger.error("Running test for: Server -> Client") + RmiCommonTest.runTests(this@onMessage, this@create, 123) + server.logger.error("Done with test for: Server -> Client") + } + } + server + } + + val client = run { + val configuration = clientConfig() + configuration.enableIPv4 = clientType.ip4 + configuration.enableIPv6 = clientType.ip6 + configuration.enableIpc = clientType.ipc +// configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java) + + val client = Client(configuration) + addEndPoint(client) + + client.onConnect { + rmi.create(23) { + client.logger.error("Running test for: Client -> Server") + RmiCommonTest.runTests(this@onConnect, this@create, 23) + client.logger.error("Done with test for: Client -> Server") + } } + + client.onMessage { m -> + client.logger.error("Received finish signal for test for: Client -> Server") + val `object` = m.testCow + val id = `object`.id() + Assert.assertEquals(123, id) + client.logger.error("Finished test for: Client -> Server") + stopEndPoints() + } + + client + } + + server.bind(2000) + when (clientType) { + ConnectType.IPC -> client.connectIpc() + ConnectType.IPC4 -> client.connect(IPv4.LOCALHOST, 2000) + ConnectType.IPC6 -> client.connect(IPv6.LOCALHOST, 2000) + ConnectType.IPC46 -> client.connect(IPv4.LOCALHOST, 2000) + ConnectType.IPC64 -> client.connect(IPv6.LOCALHOST, 2000) + ConnectType.IP4 -> client.connect(IPv4.LOCALHOST, 2000) + ConnectType.IP6 -> client.connect(IPv6.LOCALHOST, 2000) + ConnectType.IP46 -> client.connect(IPv4.LOCALHOST, 2000) + ConnectType.IP64 -> client.connect(IPv6.LOCALHOST, 2000) } waitForThreads() } + @Test + fun rmiTimeoutIpc() { + rmiBasicIpc { connection, testCow -> + RmiCommonTest.runTimeoutTest(connection, testCow) + } + } - fun rmi(isIpv4: Boolean = false, isIpv6: Boolean = false, runIpv4Connect: Boolean = true, config: Configuration.() -> Unit = {}) { - setLogLevel(Level.TRACE) + @Test + fun rmiSyncIpc() { + rmiBasicIpc { connection, testCow -> + RmiCommonTest.runSyncTest(connection, testCow) + } + } - run { + @Test + fun rmiASyncIpc() { + rmiBasicIpc { connection, testCow -> + RmiCommonTest.runASyncTest(connection, testCow) + } + } + + fun rmiBasicIpc(runFun: (Connection, TestCow) -> Unit) { + val server = run { val configuration = serverConfig() - configuration.enableIPv4 = isIpv4 - configuration.enableIPv6 = isIpv6 - config(configuration) + configuration.enableIPv4 = false + configuration.enableIPv6 = false + configuration.enableIpc = true configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java) configuration.serialization.register(MessageWithTestCow::class.java) @@ -217,7 +369,7 @@ class RmiSimpleTest : BaseTest() { val server = Server(configuration) addEndPoint(server) - server.bind() + server.onMessage { m -> server.logger.error("Received finish signal for test for: Client -> Server") @@ -231,16 +383,19 @@ class RmiSimpleTest : BaseTest() { // NOTE: THIS IS BI-DIRECTIONAL! rmi.create(123) { server.logger.error("Running test for: Server -> Client") - RmiCommonTest.runTests(this@onMessage, this, 123) + runFun(this@onMessage, this@create) server.logger.error("Done with test for: Server -> Client") } } + server } - run { + val client = run { val configuration = clientConfig() - config(configuration) -// configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java) + configuration.enableIPv4 = false + configuration.enableIPv6 = false + configuration.enableIpc = true +// configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java) val client = Client(configuration) addEndPoint(client) @@ -248,7 +403,7 @@ class RmiSimpleTest : BaseTest() { client.onConnect { rmi.create(23) { client.logger.error("Running test for: Client -> Server") - RmiCommonTest.runTests(this@onConnect, this, 23) + runFun(this@onConnect, this@create) client.logger.error("Done with test for: Client -> Server") } } @@ -259,12 +414,15 @@ class RmiSimpleTest : BaseTest() { val id = `object`.id() Assert.assertEquals(123, id) client.logger.error("Finished test for: Client -> Server") - stopEndPoints(2000) + stopEndPoints() } - doConnect(isIpv4, isIpv6, runIpv4Connect, client) + client } + server.bindIpc() + client.connectIpc() + waitForThreads() } } diff --git a/test/dorkboxTest/network/rmi/RmiSpamAsyncTest.kt b/test/dorkboxTest/network/rmi/RmiSpamAsyncTest.kt index d94e1935..97bfe46d 100644 --- a/test/dorkboxTest/network/rmi/RmiSpamAsyncTest.kt +++ b/test/dorkboxTest/network/rmi/RmiSpamAsyncTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,14 +22,10 @@ import dorkbox.network.Server import dorkbox.network.connection.Connection import dorkbox.network.rmi.RemoteObject import dorkboxTest.network.BaseTest -import kotlinx.coroutines.delay -import org.junit.Assert import org.junit.Test -import java.util.concurrent.atomic.* +import java.util.concurrent.* class RmiSpamAsyncTest : BaseTest() { - private val counter = AtomicLong(0) - private val RMI_ID = 12251 @@ -49,13 +45,12 @@ class RmiSpamAsyncTest : BaseTest() { * In this test the server has two objects in an object space. The client * uses the first remote object to get the second remote object. */ - fun rmi(config: Configuration.() -> Unit = {}) { - val server: Server - + private fun rmi(config: Configuration.() -> Unit = {}) { val mod = 100_000L - val totalRuns = 1_000_000L + val totalRuns = 1_000_000 + val latch = CountDownLatch(totalRuns) - run { + val server = run { val configuration = serverConfig() config(configuration) @@ -64,28 +59,27 @@ class RmiSpamAsyncTest : BaseTest() { configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) - server = Server(configuration) + val server = Server(configuration) addEndPoint(server) - server.rmiGlobal.save(TestObjectImpl(counter), RMI_ID) - server.bind() + server.rmiGlobal.save(TestObjectImpl(latch), RMI_ID) + server } - val client: Client - run { + val client = run { val configuration = clientConfig() config(configuration) // the logger cannot keep-up if it's on trace setLogLevel(Level.DEBUG) - client = Client(configuration) + val client = Client(configuration) addEndPoint(client) client.onConnect { val remoteObject = rmi.getGlobal(RMI_ID) - val obj = remoteObject as RemoteObject + val obj = RemoteObject.cast(remoteObject) obj.async = true var started = false @@ -107,37 +101,27 @@ class RmiSpamAsyncTest : BaseTest() { e.printStackTrace() } } - - // The async nature means that we don't know EXACTLY when all the messages will arrive. For testing, this is the closest - // we can do to attempt to have a correct info lookup. - var count = 0 - while (counter.get() < totalRuns && count < 30) { - logger.error("Waiting for ${totalRuns - counter.get()} more messages...") - count++ - delay(1_000) - } - - - // have to do this first, so it will wait for the client responses! - // if we close the client first, the connection will be closed, and the responses will never arrive to the server - stopEndPoints() } - client.connect(LOCALHOST) + client } + server.bind(2000) + client.connect(LOCALHOST, 2000) + + latch.await() + stopEndPoints() waitForThreads() - Assert.assertEquals(totalRuns, counter.get()) } private interface TestObject { - fun setOther(value: Long): Boolean + fun setOther(value: Int): Boolean } - private class TestObjectImpl(private val counter: AtomicLong) : TestObject { + private class TestObjectImpl(private val latch: CountDownLatch) : TestObject { @Override - override fun setOther(value: Long): Boolean { - counter.getAndIncrement() + override fun setOther(value: Int): Boolean { + latch.countDown() return true } } diff --git a/test/dorkboxTest/network/rmi/RmiSpamSyncSuspendingTest.kt b/test/dorkboxTest/network/rmi/RmiSpamSyncSuspendingTest.kt index 4a1a45fe..5a61d459 100644 --- a/test/dorkboxTest/network/rmi/RmiSpamSyncSuspendingTest.kt +++ b/test/dorkboxTest/network/rmi/RmiSpamSyncSuspendingTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,10 @@ import dorkbox.network.Server import dorkbox.network.connection.Connection import dorkbox.network.rmi.RemoteObject import dorkboxTest.network.BaseTest +import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Test -import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.* class RmiSpamSyncSuspendingTest : BaseTest() { private val counter = AtomicLong(0) @@ -54,37 +55,34 @@ class RmiSpamSyncSuspendingTest : BaseTest() { * In this test the server has two objects in an object space. The client * uses the first remote object to get the second remote object. */ - fun rmi(config: Configuration.() -> Unit = {}) { - val server: Server - + private fun rmi(config: Configuration.() -> Unit = {}) { val mod = 400L val totalRuns = 4_000L - run { + val server = run { val configuration = serverConfig() config(configuration) configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) - server = Server(configuration) + val server = Server(configuration) addEndPoint(server) server.rmiGlobal.save(TestObjectImpl(counter), RMI_ID) - server.bind() + server } - - val client: Client - run { + val ipc: Boolean + val client = run { val configuration = clientConfig() config(configuration) - client = Client(configuration) + val client = Client(configuration) addEndPoint(client) client.onConnect { val remoteObject = rmi.getGlobal(RMI_ID) - val obj = remoteObject as RemoteObject + val obj = RemoteObject.cast(remoteObject) obj.async = false var started = false @@ -100,7 +98,9 @@ class RmiSpamSyncSuspendingTest : BaseTest() { } try { - remoteObject.setOther(i) + runBlocking { + remoteObject.setOther(i) + } } catch (e: Exception) { logger.error("Timeout when calling RMI method") e.printStackTrace() @@ -112,9 +112,18 @@ class RmiSpamSyncSuspendingTest : BaseTest() { stopEndPoints() } - client.connect() + ipc = configuration.enableIpc + client } + server.bind(2000) + if (ipc) { + client.connectIpc() + } else { + client.connect(LOCALHOST, 2000) + } + + waitForThreads() Assert.assertEquals(totalRuns, counter.get()) } diff --git a/test/dorkboxTest/network/rmi/RmiSpamSyncTest.kt b/test/dorkboxTest/network/rmi/RmiSpamSyncTest.kt index 0d9c3e10..f510ee81 100644 --- a/test/dorkboxTest/network/rmi/RmiSpamSyncTest.kt +++ b/test/dorkboxTest/network/rmi/RmiSpamSyncTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import dorkbox.network.rmi.RemoteObject import dorkboxTest.network.BaseTest import org.junit.Assert import org.junit.Test -import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.* class RmiSpamSyncTest : BaseTest() { private val counter = AtomicLong(0) @@ -53,37 +53,34 @@ class RmiSpamSyncTest : BaseTest() { * In this test the server has two objects in an object space. The client * uses the first remote object to get the second remote object. */ - fun rmi(config: Configuration.() -> Unit = {}) { - val server: Server - + private fun rmi(config: Configuration.() -> Unit = {}) { val mod = 400L val totalRuns = 1_000L - run { + val server = run { val configuration = serverConfig() config(configuration) configuration.serialization.rmi.register(TestObject::class.java, TestObjectImpl::class.java) - server = Server(configuration) + val server = Server(configuration) addEndPoint(server) server.rmiGlobal.save(TestObjectImpl(counter), RMI_ID) - server.bind() + server } - - val client: Client - run { + var ipc: Boolean + val client = run { val configuration = clientConfig() config(configuration) - client = Client(configuration) + val client = Client(configuration) addEndPoint(client) client.onConnect { val remoteObject = rmi.getGlobal(RMI_ID) - val obj = remoteObject as RemoteObject + val obj = RemoteObject.cast(remoteObject) obj.async = false var started = false @@ -111,7 +108,15 @@ class RmiSpamSyncTest : BaseTest() { stopEndPoints() } - client.connect() + ipc = configuration.enableIpc + client + } + + server.bind(2000) + if (ipc) { + client.connectIpc() + } else { + client.connect(LOCALHOST, 2000) } waitForThreads() diff --git a/test/dorkboxTest/network/rmi/SuspendProxyTest.kt b/test/dorkboxTest/network/rmi/SuspendProxyTest.kt index a8a1c063..0ad32e2b 100644 --- a/test/dorkboxTest/network/rmi/SuspendProxyTest.kt +++ b/test/dorkboxTest/network/rmi/SuspendProxyTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 dorkbox, llc + * + * 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 dorkboxTest.network.rmi import junit.framework.TestCase import kotlinx.coroutines.delay @@ -17,7 +33,7 @@ class SuspendProxyTest : TestCase() { fun addSync(a:Int, b:Int):Int } - class SuspendHandler(private val delegate:Adder):InvocationHandler { + class SuspendHandler(private val delegate:Adder): InvocationHandler { override fun invoke(proxy: Any, method: Method, arguments: Array): Any { val suspendCoroutineObject = arguments.lastOrNull() return if (suspendCoroutineObject is Continuation<*>) { diff --git a/test/dorkboxTest/network/rmi/cows/TestCow.kt b/test/dorkboxTest/network/rmi/cows/TestCow.kt index 5e491a43..d7d22905 100644 --- a/test/dorkboxTest/network/rmi/cows/TestCow.kt +++ b/test/dorkboxTest/network/rmi/cows/TestCow.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,9 @@ package dorkboxTest.network.rmi.cows interface TestCow : TestCowBase { fun moo() fun moo(value: String) - suspend fun moo(value: String, delay: Long) + fun mooTwo(value: String): String + fun moo(value: String, delay: Long) + suspend fun mooSuspend(value: String, delay: Long) fun id(): Int suspend fun slow(): Float diff --git a/test/dorkboxTest/network/rmi/cows/TestCowImpl.kt b/test/dorkboxTest/network/rmi/cows/TestCowImpl.kt index 031bf021..8ff976a5 100644 --- a/test/dorkboxTest/network/rmi/cows/TestCowImpl.kt +++ b/test/dorkboxTest/network/rmi/cows/TestCowImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,17 +34,31 @@ open class TestCowImpl(val id: Int) : TestCowBaseImpl(), TestCow { override fun moo(value: String) { throw RuntimeException("Should never be executed!") } - fun moo(connection: Connection, value: String) { moos += 2 connection.logger.error("Moo! $moos: $value") } - override suspend fun moo(value: String, delay: Long) { + override fun mooTwo(value: String): String { +// println(value) + return "moo-two: $value" + } + + override fun moo(value: String, delay: Long) { throw RuntimeException("Should never be executed!") } - suspend fun moo(connection: Connection, value: String, delay: Long) { + fun moo(connection: Connection, value: String, delay: Long) { + moos += 4 + connection.logger.error("Moo! $moos: $value ($delay)") + Thread.sleep(delay) + } + + + override suspend fun mooSuspend(value: String, delay: Long) { + throw RuntimeException("Should never be executed!") + } + suspend fun mooSuspend(connection: Connection, value: String, delay: Long) { moos += 4 connection.logger.error("Moo! $moos: $value ($delay)") delay(delay) diff --git a/test/dorkboxTest/network/rmi/multiJVM/TestClient.kt b/test/dorkboxTest/network/rmi/multiJVM/TestClient.kt index 96dd2aa3..49ca0d8e 100644 --- a/test/dorkboxTest/network/rmi/multiJVM/TestClient.kt +++ b/test/dorkboxTest/network/rmi/multiJVM/TestClient.kt @@ -1,6 +1,6 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import dorkbox.storage.Storage import dorkboxTest.network.BaseTest import dorkboxTest.network.rmi.RmiCommonTest import dorkboxTest.network.rmi.cows.TestCow -import kotlinx.coroutines.runBlocking import org.junit.Assert import org.slf4j.LoggerFactory @@ -79,7 +78,7 @@ object TestClient { logger.error("Starting test for: Client -> Server") rmi.getGlobal(12123).apply { - RmiCommonTest.runTests(this@onConnect, this, 12123) + RmiCommonTest.runTests(this@onConnect, this@apply, 12123) logger.error("DONE") // now send this remote object ACROSS the wire to the server (on the server, this is where the IMPLEMENTATION lives) @@ -102,10 +101,7 @@ object TestClient { close() } - client.connect(BaseTest.LOCALHOST) - - runBlocking { - client.waitForClose() - } + client.connect(BaseTest.LOCALHOST, 2000) + client.waitForClose() } } diff --git a/test/dorkboxTest/network/rmi/multiJVM/TestServer.kt b/test/dorkboxTest/network/rmi/multiJVM/TestServer.kt index cdffa1c5..67231f47 100644 --- a/test/dorkboxTest/network/rmi/multiJVM/TestServer.kt +++ b/test/dorkboxTest/network/rmi/multiJVM/TestServer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 dorkbox, llc + * Copyright 2023 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import dorkboxTest.network.rmi.cows.TestBabyCowImpl import dorkboxTest.network.rmi.cows.TestCow import dorkboxTest.network.rmi.cows.TestCowImpl import dorkboxTest.network.rmi.multiJVM.TestClient.setup -import kotlinx.coroutines.runBlocking import org.junit.Assert /** @@ -87,10 +86,7 @@ object TestServer { // } } - server.bind() - - runBlocking { - server.waitForClose() - } + server.bind(2000) + server.waitForClose() } }