├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── issue-template.md ├── PULL_REQUEST_TEMPLATE.md ├── release.yml └── workflows │ ├── main.yml │ ├── pull_request.yml │ └── pull_request_label.yml ├── .gitignore ├── .licenseignore ├── .spi.yml ├── .swift-format ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── HANDBOOK.md ├── LICENSE.txt ├── NOTICE.txt ├── Package.swift ├── README.md ├── Samples ├── .gitignore ├── Package.swift ├── README.md ├── Sources │ └── SWIMNIOSampleCluster │ │ ├── SWIMNIOSampleNode.swift │ │ └── main.swift └── Tests │ └── NoopTests │ └── SampleTest.swift ├── Sources ├── ClusterMembership │ └── Node.swift ├── SWIM │ ├── Docs.docc │ │ ├── images │ │ │ ├── ping_pingreq_cycle.graffle │ │ │ ├── ping_pingreq_cycle.png │ │ │ ├── ping_pingreq_cycle.svg │ │ │ ├── swim_lifecycle.graffle │ │ │ ├── swim_lifecycle.png │ │ │ └── swim_lifecycle.svg │ │ └── index.md │ ├── Events.swift │ ├── Member.swift │ ├── Metrics.swift │ ├── Peer.swift │ ├── SWIM.swift │ ├── SWIMInstance.swift │ ├── SWIMProtocol.swift │ ├── Settings.swift │ ├── Status.swift │ └── Utils │ │ ├── Heap.swift │ │ ├── String+Extensions.swift │ │ ├── _PrettyLog.swift │ │ └── time.swift └── SWIMNIOExample │ ├── Coding.swift │ ├── Logging.swift │ ├── Message.swift │ ├── NIOPeer.swift │ ├── SWIMNIOHandler.swift │ ├── SWIMNIOShell.swift │ ├── Settings.swift │ └── Utils │ ├── String+Extensions.swift │ └── time.swift ├── Tests ├── ClusterMembershipDocumentationTests │ └── SWIMDocExamples.swift ├── ClusterMembershipTests │ └── NodeTests.swift ├── SWIMNIOExampleTests │ ├── CodingTests.swift │ ├── SWIMNIOClusteredTests.swift │ ├── SWIMNIOEventClusteredTests.swift │ ├── SWIMNIOMetricsTests.swift │ └── Utils │ │ └── BaseXCTestCases.swift ├── SWIMTestKit │ ├── LogCapture.swift │ └── TestMetrics.swift └── SWIMTests │ ├── HeapTests.swift │ ├── SWIMInstanceTests.swift │ ├── SWIMMetricsTests.swift │ ├── SWIMSettingsTests.swift │ └── TestPeer.swift └── dev └── git.commit.template /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue Template 3 | about: Template for reporting general issues with the library 4 | title: '' 5 | labels: 0 - new 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Expected behavior 11 | 12 | 13 | ### Actual behavior 14 | 15 | 16 | ### Steps to reproduce 17 | 18 | 19 | 20 | ### If possible, minimal yet complete reproducer code (or URL to code) 21 | 22 | 23 | 24 | ### Version/commit hash 25 | 26 | 27 | 28 | ### Swift & OS version (output of `swift --version && uname -a`) 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | _[One line description of your change]_ 2 | 3 | ### Motivation: 4 | 5 | _[Explain here the context, and why you're making that change. What is the problem you're trying to solve.]_ 6 | 7 | ### Modifications: 8 | 9 | _[Describe the modifications you've done.]_ 10 | 11 | ### Result: 12 | 13 | - Resolves # 14 | - _[After your change, what will change.]_ 15 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: SemVer Major 4 | labels: 5 | - ⚠️ semver/major 6 | - title: SemVer Minor 7 | labels: 8 | - 🆕 semver/minor 9 | - title: SemVer Patch 10 | labels: 11 | - 🔨 semver/patch 12 | - title: Other Changes 13 | labels: 14 | - semver/none 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | schedule: 7 | - cron: "0 8,20 * * *" 8 | 9 | jobs: 10 | unit-tests: 11 | name: Unit tests 12 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 13 | with: 14 | linux_5_10_arguments_override: "--explicit-target-dependency-import-check error" 15 | linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 16 | linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 17 | linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 18 | linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 19 | 20 | construct-samples-matrix: 21 | name: Construct samples matrix 22 | runs-on: ubuntu-latest 23 | outputs: 24 | samples-matrix: '${{ steps.generate-matrix.outputs.samples-matrix }}' 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | with: 29 | persist-credentials: false 30 | - id: generate-matrix 31 | run: echo "samples-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT" 32 | env: 33 | MATRIX_LINUX_COMMAND: "swift build --package-path Samples --explicit-target-dependency-import-check error" 34 | 35 | samples: 36 | name: Samples 37 | needs: construct-samples-matrix 38 | uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main 39 | with: 40 | name: "Samples" 41 | matrix_string: '${{ needs.construct-samples-matrix.outputs.samples-matrix }}' 42 | 43 | macos-tests: 44 | name: macOS tests 45 | uses: apple/swift-nio/.github/workflows/macos_tests.yml@main 46 | with: 47 | build_scheme: "none" # no defined build schemes 48 | macos_xcode_build_enabled: false 49 | ios_xcode_build_enabled: false 50 | watchos_xcode_build_enabled: false 51 | tvos_xcode_build_enabled: false 52 | visionos_xcode_build_enabled: false 53 | 54 | static-sdk: 55 | name: Static SDK 56 | # Workaround https://github.com/nektos/act/issues/1875 57 | uses: apple/swift-nio/.github/workflows/static_sdk.yml@main 58 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | 7 | jobs: 8 | soundness: 9 | name: Soundness 10 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main 11 | with: 12 | license_header_check_project_name: "Swift Cluster Membership" 13 | 14 | unit-tests: 15 | name: Unit tests 16 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 17 | with: 18 | linux_5_10_arguments_override: "--explicit-target-dependency-import-check error" 19 | linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 20 | linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 21 | linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 22 | linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 23 | 24 | construct-samples-matrix: 25 | name: Construct samples matrix 26 | runs-on: ubuntu-latest 27 | outputs: 28 | samples-matrix: '${{ steps.generate-matrix.outputs.samples-matrix }}' 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | with: 33 | persist-credentials: false 34 | - id: generate-matrix 35 | run: echo "samples-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT" 36 | env: 37 | MATRIX_LINUX_COMMAND: "swift build --package-path Samples --explicit-target-dependency-import-check error" 38 | 39 | samples: 40 | name: Samples 41 | needs: construct-samples-matrix 42 | uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main 43 | with: 44 | name: "Samples" 45 | matrix_string: '${{ needs.construct-samples-matrix.outputs.samples-matrix }}' 46 | 47 | cxx-interop: 48 | name: Cxx interop 49 | uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main 50 | 51 | static-sdk: 52 | name: Static SDK 53 | # Workaround https://github.com/nektos/act/issues/1875 54 | uses: apple/swift-nio/.github/workflows/static_sdk.yml@main 55 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_label.yml: -------------------------------------------------------------------------------- 1 | name: PR label 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, unlabeled, opened, reopened, synchronize] 6 | 7 | jobs: 8 | semver-label-check: 9 | name: Semantic version label check 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 1 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | with: 16 | persist-credentials: false 17 | - name: Check for Semantic Version label 18 | uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .swift-version 3 | 4 | *.orig 5 | *.app 6 | 7 | /.build 8 | /Samples/.build 9 | /.SourceKitten 10 | /Packages 11 | .xcode 12 | *.app 13 | /*.xcodeproj 14 | Samples/swift-cluster-membership-samples.xcodeproj 15 | .xcode 16 | .idea 17 | Samples/swift-cluster-membership-samples.xcodeproj 18 | 19 | # rendered docs output dirs 20 | /reference/ 21 | /api/ 22 | .swiftpm/ 23 | Package.resolved 24 | 25 | -------------------------------------------------------------------------------- /.licenseignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | **/.gitignore 3 | .licenseignore 4 | .gitattributes 5 | .git-blame-ignore-revs 6 | .mailfilter 7 | .mailmap 8 | .spi.yml 9 | .swift-format 10 | .editorconfig 11 | .github/* 12 | *.md 13 | *.txt 14 | *.yml 15 | *.yaml 16 | *.json 17 | Package.swift 18 | **/Package.swift 19 | Package@-*.swift 20 | **/Package@-*.swift 21 | Package.resolved 22 | **/Package.resolved 23 | Makefile 24 | *.modulemap 25 | **/*.modulemap 26 | **/*.docc/* 27 | *.xcprivacy 28 | **/*.xcprivacy 29 | *.symlink 30 | **/*.symlink 31 | Dockerfile 32 | **/Dockerfile 33 | Snippets/* 34 | dev/git.commit.template 35 | .unacceptablelanguageignore 36 | IntegrationTests/*.sh 37 | Sources/SWIM/Utils/Heap.swift 38 | Tests/SWIMTests/HeapTests.swift 39 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [ClusterMembership, SWIM] 5 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | 3 | 4 | "version" : 1, 5 | "indentation" : { 6 | "spaces" : 4 7 | }, 8 | "tabWidth" : 4, 9 | "fileScopedDeclarationPrivacy" : { 10 | "accessLevel" : "private" 11 | }, 12 | "spacesAroundRangeFormationOperators" : false, 13 | "indentConditionalCompilationBlocks" : false, 14 | "indentSwitchCaseLabels" : false, 15 | "lineBreakAroundMultilineExpressionChainComponents" : false, 16 | "lineBreakBeforeControlFlowKeywords" : false, 17 | "lineBreakBeforeEachArgument" : true, 18 | "lineBreakBeforeEachGenericRequirement" : true, 19 | "lineLength" : 240, 20 | "maximumBlankLines" : 1, 21 | "respectsExistingLineBreaks" : true, 22 | "prioritizeKeepingFunctionOutputTogether" : true, 23 | "noAssignmentInExpressions" : { 24 | "allowedFunctions" : [ 25 | "XCTAssertNoThrow", 26 | "XCTAssertThrowsError" 27 | ] 28 | }, 29 | "rules" : { 30 | "NoBlockComments" : false, 31 | "ReplaceForEachWithForLoop" : false, 32 | 33 | "AllPublicDeclarationsHaveDocumentation" : false, 34 | "AlwaysUseLiteralForEmptyCollectionInit" : false, 35 | "AlwaysUseLowerCamelCase" : false, 36 | "AmbiguousTrailingClosureOverload" : true, 37 | "BeginDocumentationCommentWithOneLineSummary" : false, 38 | "DoNotUseSemicolons" : true, 39 | "DontRepeatTypeInStaticProperties" : true, 40 | "FileScopedDeclarationPrivacy" : true, 41 | "FullyIndirectEnum" : true, 42 | "GroupNumericLiterals" : true, 43 | "IdentifiersMustBeASCII" : true, 44 | "NeverForceUnwrap" : false, 45 | "NeverUseForceTry" : false, 46 | "NeverUseImplicitlyUnwrappedOptionals" : false, 47 | "NoAccessLevelOnExtensionDeclaration" : true, 48 | "NoAssignmentInExpressions" : true, 49 | "NoBlockComments" : true, 50 | "NoCasesWithOnlyFallthrough" : true, 51 | "NoEmptyTrailingClosureParentheses" : true, 52 | "NoLabelsInCasePatterns" : true, 53 | "NoLeadingUnderscores" : false, 54 | "NoParensAroundConditions" : true, 55 | "NoVoidReturnOnFunctionSignature" : true, 56 | "OmitExplicitReturns" : true, 57 | "OneCasePerLine" : true, 58 | "OneVariableDeclarationPerLine" : true, 59 | "OnlyOneTrailingClosureArgument" : true, 60 | "OrderedImports" : true, 61 | "ReplaceForEachWithForLoop" : true, 62 | "ReturnVoidInsteadOfEmptyTuple" : true, 63 | "UseEarlyExits" : false, 64 | "UseExplicitNilCheckInConditions" : false, 65 | "UseLetInEveryBoundCaseVariable" : false, 66 | "UseShorthandTypeNames" : true, 67 | "UseSingleLinePropertyGetter" : false, 68 | "UseSynthesizedInitializer" : false, 69 | "UseTripleSlashForDocumentationComments" : true, 70 | "UseWhereClausesInForLoops" : false, 71 | "ValidateDocumentationComments" : false 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The code of conduct for this project can be found at https://swift.org/code-of-conduct. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license 4 | your contribution to Apple and the community, and agree by submitting the patch 5 | that your contributions are licensed under the Apache 2.0 license (see 6 | `LICENSE.txt`). 7 | 8 | 9 | ## How to submit a bug report 10 | 11 | Please ensure to specify the following: 12 | 13 | * Swift Cluster Membership commit hash 14 | * Contextual information (e.g. what you were trying to achieve with Swift Cluster Membership) 15 | * Simplest possible steps to reproduce 16 | * More complex the steps are, lower the priority will be. 17 | * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description. 18 | * Anything that might be relevant in your opinion, such as: 19 | * Swift version or the output of `swift --version` 20 | * OS version and the output of `uname -a` 21 | * Network configuration 22 | 23 | 24 | ### Example 25 | 26 | ``` 27 | Swift Cluster Membership commit hash: 22ec043dc9d24bb011b47ece4f9ee97ee5be2757 28 | 29 | Context: 30 | While load testing my HTTP web server written with Swift Cluster Membership, I noticed 31 | that one file descriptor is leaked per request. 32 | 33 | Steps to reproduce: 34 | 1. ... 35 | 2. ... 36 | 3. ... 37 | 4. ... 38 | 39 | $ swift --version 40 | Swift version 4.0.2 (swift-4.0.2-RELEASE) 41 | Target: x86_64-unknown-linux-gnu 42 | 43 | Operating system: Ubuntu Linux 16.04 64-bit 44 | 45 | $ uname -a 46 | Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Nov 10 18:29:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux 47 | 48 | My system has IPv6 disabled. 49 | ``` 50 | 51 | ## Writing a Patch 52 | 53 | A good Swift Cluster Membership patch is: 54 | 55 | 1. Concise, and contains as few changes as needed to achieve the end result. 56 | 2. Tested, ensuring that any tests provided failed before the patch and pass after it. 57 | 3. Documented, adding API documentation as needed to cover new functions and properties. 58 | 4. Adheres to our code formatting conventions and [style guide](STYLE_GUIDE.md). 59 | 5. Accompanied by a great commit message, using our commit message template. 60 | 61 | ### Code Format and Style 62 | 63 | Swift Cluster Membership uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) to enforce the preferred [swift code format](.swiftformat). Always run SwiftFormat before committing your code. 64 | 65 | ### Commit Message Template 66 | 67 | We require that your commit messages match our template. The easiest way to do that is to get git to help you by explicitly using the template. To do that, `cd` to the root of our repository and run: 68 | 69 | git config commit.template dev/git.commit.template 70 | 71 | ### Run CI checks locally 72 | 73 | You can run the GitHub Actions workflows locally using [act](https://github.com/nektos/act). For detailed steps on how to do this please see [https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally](https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally). 74 | 75 | ## How to contribute your work 76 | 77 | Please open a pull request at https://github.com/apple/swift-cluster-membership. Make sure the CI passes, and then wait for code review. 78 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | For the purpose of tracking copyright, this is the list of individuals and 2 | organizations who have contributed source code to Swift Cluster Membership. 3 | 4 | For employees of an organization/company where the copyright of work done 5 | by employees of that company is held by the company itself, only the company 6 | needs to be listed here. 7 | 8 | ## COPYRIGHT HOLDERS 9 | 10 | - Apple Inc. (all contributors with '@apple.com') 11 | 12 | ### Contributors 13 | 14 | - Konrad `ktoso` Malawski 15 | - Dario Rexin 16 | - Anton Volokhov 17 | - Tom Doron 18 | -------------------------------------------------------------------------------- /HANDBOOK.md: -------------------------------------------------------------------------------- 1 | # Contributors Handbook 2 | 3 | ## Glossary 4 | 5 | In an attempt to form a shared vocabulary in the project, words used in the APIs have been carefully selected and we'd like to keep using them to refer to the same things in various implementations. 6 | 7 | - CoolAlgorithm **Instance** - we refer to a specific implementation of a membership algorithm as an "Instance". An instance is: 8 | - SHOULD be a value type; as it makes debugging the state of an instance simpler; i.e. we can record the last 10 states of the algorithm and see how things went wrong, even in running clustered systems if necessary. 9 | - is NOT performing any IO (except for logging which we after long debates, decided that it's better to allow implementations to log if configured to do so) 10 | - it most likely IS a finite state machine, although may not be one in the strict meaning of the pattern. If possible to express as an FSM, we recommend doing so, or carving out pieces of the protocol which can be represented as a state machine. 11 | - an instance SHOULD be easy to test in isolation, such that crazy edge cases in algorithms can be codified in easy to run and understand test cases when necessary 12 | - CoolAlgorithm **Shell** - inspired by shells as we know them from terminals, a shell is what handles the interaction with the environment and the instance; it is the link between the I/O and the pure instance. One can also think of it as an "interpreter." 13 | - **Directive** - directives are how an algorithm instance may choose to interact with a Shell. Upon performing some action on an algorithm's instance it SHOULD return a directive, which will instruct the Shell to "now do this thing, then that thing". It "directs the shell" to do the right thing, following the instances protocol. 14 | - **Peer** - a peer is a known host that has the potential to be a cluster member; we can communicate with a peer by sending messages to it (and it may send messages to us), however a peer does not have an inherent cluster membership status, in order to have a status it must be (wrapped in a) *Member* 15 | - Peers SHOULD be Unique; meaning that if node dies and spawns again using the same host/port pair, we should consider it to be a _new peer_ rather than the same peer. This is usually solved by issuing some random UUID on node startup, and including this ID in any messaging the peer performs. 16 | - **Cluster Member** - a member of a cluster, meaning it is _known to be (or have been) part of the cluster_ and likely has some associated cluster state (e.g. alive or dead etc.) 17 | - It most likely is wrapping a Peer with additional information 18 | 19 | ## Tips 20 | 21 | - When working with directives, never `return []`, always preallocate a directives array and `return directives`. 22 | - there are many situations where it is good to bail out early, but many operations have some form of "needs to always be done" 23 | in their directives. Using this pattern ensures you won't accidentally miss those directives. 24 | - When "should never happen", use `precondition`s with a lot of contextual information (including the entire instance state), 25 | so users can provide you with a good crash report. 26 | 27 | ## Testing tips 28 | 29 | Tests have `LogCapture` installed are able to capture all logs "per node" and later present them in a readable output if a test fails automatically. 30 | 31 | If you need to investigate test logs without the test failing, you can enable them like so: 32 | 33 | ```swift 34 | final class SWIMNIOClusteredTests: RealClusteredXCTestCase { 35 | 36 | override var alwaysPrintCaptureLogs: Bool { 37 | true 38 | } 39 | 40 | // ... 41 | 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | 2 | The Swift Cluster Membership Project 3 | ==================================== 4 | 5 | Please visit the Swift Cluster Membership web site for more information: 6 | 7 | * https://github.com/apple/swift-cluster-membership 8 | 9 | Copyright 2020 The Swift Cluster Membership Project 10 | 11 | The Swift Cluster Membership Project licenses this file to you under the Apache License, 12 | version 2.0 (the "License"); you may not use this file except in compliance 13 | with the License. You may obtain a copy of the License at: 14 | 15 | https://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 19 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 20 | License for the specific language governing permissions and limitations 21 | under the License. 22 | 23 | Also, please refer to each LICENSE..txt file, which is located in 24 | the 'license' directory of the distribution file, for the license terms of the 25 | components that this product depends on. 26 | 27 | --- 28 | 29 | This product contains some modified data-structures from Apple/Swift-NIO. 30 | 31 | * LICENSE (Apache 2.0): 32 | * https://github.com/apple/swift-nio/blob/main/LICENSE.txt 33 | * HOMEPAGE: 34 | * https://github.com/apple/swift-nio 35 | 36 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | import class Foundation.ProcessInfo 7 | 8 | // Workaround: Since we cannot include the flat just as command line options since then it applies to all targets, 9 | // and ONE of our dependencies currently produces one warning, we have to use this workaround to enable it in _our_ 10 | // targets when the flag is set. We should remove the dependencies and then enable the flag globally though just by passing it. 11 | let globalSwiftSettings: [SwiftSetting] 12 | if ProcessInfo.processInfo.environment["WARNINGS_AS_ERRORS"] != nil { 13 | print("WARNINGS_AS_ERRORS enabled, passing `-warnings-as-errors`") 14 | globalSwiftSettings = [ 15 | SwiftSetting.unsafeFlags(["-warnings-as-errors"]) 16 | ] 17 | } else { 18 | globalSwiftSettings = [] 19 | } 20 | 21 | var targets: [PackageDescription.Target] = [ 22 | // ==== ------------------------------------------------------------------------------------------------------------ 23 | // MARK: SWIM 24 | 25 | .target( 26 | name: "ClusterMembership", 27 | dependencies: [] 28 | ), 29 | 30 | .target( 31 | name: "SWIM", 32 | dependencies: [ 33 | "ClusterMembership", 34 | .product(name: "Logging", package: "swift-log"), 35 | .product(name: "Metrics", package: "swift-metrics"), 36 | ] 37 | ), 38 | 39 | .target( 40 | name: "SWIMNIOExample", 41 | dependencies: [ 42 | "SWIM", 43 | .product(name: "NIO", package: "swift-nio"), 44 | .product(name: "NIOFoundationCompat", package: "swift-nio"), 45 | .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), 46 | .product(name: "NIOExtras", package: "swift-nio-extras"), 47 | 48 | .product(name: "Logging", package: "swift-log"), 49 | .product(name: "Metrics", package: "swift-metrics"), 50 | ] 51 | ), 52 | 53 | // ==== ------------------------------------------------------------------------------------------------------------ 54 | // MARK: Other Membership Protocols ... 55 | 56 | // ==== ------------------------------------------------------------------------------------------------------------ 57 | // MARK: Documentation 58 | 59 | .testTarget( 60 | name: "ClusterMembershipDocumentationTests", 61 | dependencies: [ 62 | "SWIM" 63 | ] 64 | ), 65 | 66 | // ==== ------------------------------------------------------------------------------------------------------------ 67 | // MARK: Tests 68 | 69 | .testTarget( 70 | name: "ClusterMembershipTests", 71 | dependencies: [ 72 | "ClusterMembership" 73 | ] 74 | ), 75 | 76 | .testTarget( 77 | name: "SWIMTests", 78 | dependencies: [ 79 | "SWIM", 80 | "SWIMTestKit", 81 | ] 82 | ), 83 | 84 | .testTarget( 85 | name: "SWIMNIOExampleTests", 86 | dependencies: [ 87 | "SWIMNIOExample", 88 | "SWIMTestKit", 89 | ] 90 | ), 91 | 92 | // NOT FOR PUBLIC CONSUMPTION. 93 | .testTarget( 94 | name: "SWIMTestKit", 95 | dependencies: [ 96 | "SWIM", 97 | .product(name: "NIO", package: "swift-nio"), 98 | .product(name: "Logging", package: "swift-log"), 99 | .product(name: "Metrics", package: "swift-metrics"), 100 | ] 101 | ), 102 | 103 | // ==== ------------------------------------------------------------------------------------------------------------ 104 | // MARK: Samples are defined in Samples/Package.swift 105 | // ==== ------------------------------------------------------------------------------------------------------------ 106 | ] 107 | 108 | var dependencies: [Package.Dependency] = [ 109 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.19.0"), 110 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.8.0"), 111 | .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.5.1"), 112 | 113 | // ~~~ SSWG APIs ~~~ 114 | .package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"), 115 | .package(url: "https://github.com/apple/swift-metrics.git", "2.3.2"..<"3.0.0"), // since latest 116 | 117 | ] 118 | 119 | let products: [PackageDescription.Product] = [ 120 | .library( 121 | name: "ClusterMembership", 122 | targets: ["ClusterMembership"] 123 | ), 124 | .library( 125 | name: "SWIM", 126 | targets: ["SWIM"] 127 | ), 128 | .library( 129 | name: "SWIMNIOExample", 130 | targets: ["SWIMNIOExample"] 131 | ), 132 | ] 133 | 134 | var package = Package( 135 | name: "swift-cluster-membership", 136 | platforms: [ 137 | .macOS(.v13), 138 | .iOS(.v16), 139 | .tvOS(.v16), 140 | .watchOS(.v9), 141 | ], 142 | products: products, 143 | 144 | dependencies: dependencies, 145 | 146 | targets: targets.map { target in 147 | var swiftSettings = target.swiftSettings ?? [] 148 | swiftSettings.append(contentsOf: globalSwiftSettings) 149 | if !swiftSettings.isEmpty { 150 | target.swiftSettings = swiftSettings 151 | } 152 | return target 153 | }, 154 | 155 | cxxLanguageStandard: .cxx11 156 | ) 157 | 158 | // --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // 159 | for target in package.targets { 160 | switch target.type { 161 | case .regular, .test, .executable: 162 | var settings = target.swiftSettings ?? [] 163 | // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md 164 | settings.append(.enableUpcomingFeature("MemberImportVisibility")) 165 | target.swiftSettings = settings 166 | case .macro, .plugin, .system, .binary: 167 | () // not applicable 168 | @unknown default: 169 | () // we don't know what to do here, do nothing 170 | } 171 | } 172 | // --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // 173 | -------------------------------------------------------------------------------- /Samples/.gitignore: -------------------------------------------------------------------------------- 1 | # The XPC sample generates an .app, so we want to ignore it 2 | *.app 3 | -------------------------------------------------------------------------------- /Samples/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | var targets: [PackageDescription.Target] = [ 7 | .target( 8 | name: "SWIMNIOSampleCluster", 9 | dependencies: [ 10 | .product(name: "SWIM", package: "swift-cluster-membership"), 11 | .product(name: "SWIMNIOExample", package: "swift-cluster-membership"), 12 | .product(name: "SwiftPrometheus", package: "SwiftPrometheus"), 13 | .product(name: "Lifecycle", package: "swift-service-lifecycle"), 14 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 15 | ], 16 | path: "Sources/SWIMNIOSampleCluster" 17 | ), 18 | 19 | /* --- tests --- */ 20 | 21 | // no-tests placeholder project to not have `swift test` fail on Samples/ 22 | .testTarget( 23 | name: "NoopTests", 24 | dependencies: [ 25 | .product(name: "SWIM", package: "swift-cluster-membership") 26 | ], 27 | path: "Tests/NoopTests" 28 | ), 29 | ] 30 | 31 | var dependencies: [Package.Dependency] = [ 32 | // ~~~~~~~ parent ~~~~~~~ 33 | .package(path: "../"), 34 | 35 | // ~~~~~~~ only for samples ~~~~~~~ 36 | 37 | .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "1.0.0-alpha"), 38 | .package(url: "https://github.com/MrLotU/SwiftPrometheus.git", from: "1.0.0-alpha"), 39 | .package(url: "https://github.com/apple/swift-argument-parser", from: "0.2.0"), 40 | ] 41 | 42 | let package = Package( 43 | name: "swift-cluster-membership-samples", 44 | platforms: [ 45 | .macOS(.v13) 46 | ], 47 | products: [ 48 | .executable( 49 | name: "SWIMNIOSampleCluster", 50 | targets: ["SWIMNIOSampleCluster"] 51 | ) 52 | 53 | ], 54 | 55 | dependencies: dependencies, 56 | 57 | targets: targets, 58 | 59 | cxxLanguageStandard: .cxx11 60 | ) 61 | -------------------------------------------------------------------------------- /Samples/README.md: -------------------------------------------------------------------------------- 1 | ## Sample applications 2 | 3 | Use `swift run` to run the samples. 4 | 5 | ### SWIMNIOSampleCluster 6 | 7 | This sample app runs a _single node_ per process, however it is prepared to be easily clustered up. 8 | This mode of operation is useful to manually suspend or stop processes and see those issues be picked up by the SWIM implementation. 9 | 10 | Recommended way to run: 11 | 12 | ```bash 13 | # cd swift-cluster-membership 14 | > swift run --package-path Samples SWIMNIOSampleCluster --help 15 | ``` 16 | 17 | which uses [swift argument parser](https://github.com/apple/swift-argument-parser) list all the options the sample app has available. 18 | 19 | An example invocation would be: 20 | 21 | ```bash 22 | swift run --package-path Samples SWIMNIOSampleCluster --port 7001 23 | ``` 24 | 25 | which spawns a node on `127.0.0.1:7001`, to spawn another node to join it and form a two node cluster you can: 26 | 27 | ```bash 28 | swift run --package-path Samples SWIMNIOSampleCluster --port 7002 --initial-contact-points 127.0.0.1:7001,127.0.0.1:8888 29 | 30 | # you can list multiple peers as contact points like this: 31 | swift run --package-path Samples SWIMNIOSampleCluster --port 7003 --initial-contact-points 127.0.0.1:7001,127.0.0.1:7002 32 | ``` 33 | 34 | Once the cluster is formed, you'll see messages logged by the `SWIMNIOSampleHandler` showing when nodes become alive or dead. 35 | 36 | You can enable debug or trace level logging to inspect more of the details of what is going on internally in the nodes. 37 | 38 | To see the failure detection in action, you can stop processes, or "suspend" them for a little while by doing 39 | 40 | ```bash 41 | # swift run --package-path Samples SWIMNIOSampleCluster --port 7001 42 | 43 | ^Z 44 | [1] + 35002 suspended swift run --package-path Samples SWIMNIOSampleCluster --port 7001 45 | $ fg %1 46 | ``` 47 | 48 | to resume the node; This can be useful to poke around and manually get a feel about how the failure detection works. 49 | You can also hook the systems up to a metrics dashboard to see the information propagate in real time (once it is instrumented using swift-metrics). 50 | -------------------------------------------------------------------------------- /Samples/Sources/SWIMNIOSampleCluster/SWIMNIOSampleNode.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import Logging 17 | import NIO 18 | import SWIM 19 | import SWIMNIOExample 20 | 21 | struct SampleSWIMNIONode { 22 | let port: Int 23 | var settings: SWIMNIO.Settings 24 | 25 | let group: EventLoopGroup 26 | 27 | init(port: Int, settings: SWIMNIO.Settings, group: EventLoopGroup) { 28 | self.port = port 29 | self.settings = settings 30 | self.group = group 31 | } 32 | 33 | func start() { 34 | let bootstrap = DatagramBootstrap(group: group) 35 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 36 | .channelInitializer { channel in 37 | channel.pipeline 38 | .addHandler(SWIMNIOHandler(settings: self.settings)).flatMap { 39 | channel.pipeline.addHandler(SWIMNIOSampleHandler()) 40 | } 41 | } 42 | 43 | bootstrap.bind(host: "127.0.0.1", port: port).whenComplete { result in 44 | switch result { 45 | case .success(let res): 46 | self.settings.logger.info("Bound to: \(res)") 47 | () 48 | case .failure(let error): 49 | self.settings.logger.error("Error: \(error)") 50 | () 51 | } 52 | } 53 | } 54 | 55 | } 56 | 57 | final class SWIMNIOSampleHandler: ChannelInboundHandler { 58 | typealias InboundIn = SWIM.MemberStatusChangedEvent 59 | 60 | let log = Logger(label: "SWIMNIOSample") 61 | 62 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 63 | let change: SWIM.MemberStatusChangedEvent = self.unwrapInboundIn(data) 64 | 65 | // we log each event (in a pretty way) 66 | self.log.info( 67 | "Membership status changed: [\(change.member.node)] is now [\(change.status)]", 68 | metadata: [ 69 | "swim/member": "\(change.member.node)", 70 | "swim/member/previousStatus": "\(change.previousStatus.map({"\($0)"}) ?? "unknown")", 71 | "swim/member/status": "\(change.status)", 72 | ] 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Samples/Sources/SWIMNIOSampleCluster/main.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ArgumentParser 16 | import ClusterMembership 17 | import Lifecycle 18 | import Logging 19 | import Metrics 20 | import NIO 21 | import Prometheus 22 | import SWIM 23 | import SWIMNIOExample 24 | 25 | struct SWIMNIOSampleCluster: ParsableCommand { 26 | @Option(name: .shortAndLong, help: "The number of nodes to start, defaults to: 1") 27 | var count: Int? 28 | 29 | @Argument(help: "Hostname that node(s) should bind to") 30 | var host: String? 31 | 32 | @Option(help: "Determines which this node should bind to; Only effective when running a single node") 33 | var port: Int? 34 | 35 | @Option(help: "Configures which nodes should be passed in as initial contact points, format: host:port,") 36 | var initialContactPoints: String = "" 37 | 38 | @Option(help: "Configures log level") 39 | var logLevel: String = "info" 40 | 41 | mutating func run() throws { 42 | LoggingSystem.bootstrap(_SWIMPrettyMetadataLogHandler.init) 43 | let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) 44 | 45 | // Uncomment this if you'd like to see metrics displayed in the command line periodically; 46 | // This bootstraps and uses the Prometheus metrics backend to report metrics periodically by printing them to the stdout (console). 47 | // 48 | // Note though that this will be a bit noisy, since logs are also emitted to the stdout by default, however it's a nice way 49 | // to learn and explore what the metrics are and how they behave when toying around with a local cluster. 50 | // let prom = PrometheusClient() 51 | // MetricsSystem.bootstrap(prom) 52 | // 53 | // group.next().scheduleRepeatedTask(initialDelay: .seconds(1), delay: .seconds(10)) { _ in 54 | // prom.collect { (string: String) in 55 | // print("") 56 | // print("") 57 | // print(string) 58 | // } 59 | // } 60 | 61 | let lifecycle = ServiceLifecycle() 62 | lifecycle.registerShutdown( 63 | label: "eventLoopGroup", 64 | .sync(group.syncShutdownGracefully) 65 | ) 66 | 67 | var settings = SWIMNIO.Settings() 68 | if count == nil || count == 1 { 69 | let nodePort = self.port ?? 7001 70 | settings.logger = Logger(label: "swim-\(nodePort)") 71 | settings.logger.logLevel = self.parseLogLevel() 72 | settings.swim.logger.logLevel = self.parseLogLevel() 73 | 74 | settings.swim.initialContactPoints = self.parseContactPoints() 75 | 76 | let node = SampleSWIMNIONode(port: nodePort, settings: settings, group: group) 77 | lifecycle.register( 78 | label: "swim-\(nodePort)", 79 | start: .sync { node.start() }, 80 | shutdown: .sync {} 81 | ) 82 | 83 | } else { 84 | let basePort = port ?? 7001 85 | for i in 1...(count ?? 1) { 86 | let nodePort = basePort + i 87 | 88 | settings.logger = Logger(label: "swim-\(nodePort)") 89 | settings.swim.initialContactPoints = self.parseContactPoints() 90 | 91 | let node = SampleSWIMNIONode( 92 | port: nodePort, 93 | settings: settings, 94 | group: group 95 | ) 96 | 97 | lifecycle.register( 98 | label: "swim\(nodePort)", 99 | start: .sync { node.start() }, 100 | shutdown: .sync {} 101 | ) 102 | } 103 | } 104 | 105 | try lifecycle.startAndWait() 106 | } 107 | 108 | private func parseLogLevel() -> Logger.Level { 109 | guard let level = Logger.Level.init(rawValue: self.logLevel) else { 110 | fatalError("Unknown log level: \(self.logLevel)") 111 | } 112 | return level 113 | } 114 | 115 | private func parseContactPoints() -> Set { 116 | guard self.initialContactPoints.trimmingCharacters(in: .whitespacesAndNewlines) != "" else { 117 | return [] 118 | } 119 | 120 | let contactPoints: [Node] = self.initialContactPoints.split(separator: ",").map { hostPort in 121 | let host = String(hostPort.split(separator: ":")[0]) 122 | let port = Int(String(hostPort.split(separator: ":")[1]))! 123 | 124 | return Node(protocol: "udp", host: host, port: port, uid: nil) 125 | } 126 | 127 | return Set(contactPoints) 128 | } 129 | } 130 | 131 | SWIMNIOSampleCluster.main() 132 | -------------------------------------------------------------------------------- /Samples/Tests/NoopTests/SampleTest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import SWIM 16 | import XCTest 17 | 18 | final class SampleTest: XCTestCase { 19 | func test_empty() { 20 | // nothing here (so far...) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ClusterMembership/Node.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// Generic representation of a (potentially unique, if `uid` is present) node in a cluster. 16 | /// 17 | /// Generally the node represents "some node we want to contact" if the `uid` is not set, 18 | /// and if the `uid` is available "the specific instance of a node". 19 | public struct Node: Hashable, Sendable, Comparable, CustomStringConvertible { 20 | /// Protocol that can be used to contact this node; 21 | /// Does not have to be a formal protocol name and may be "swim" or a name which is understood by a membership implementation. 22 | public var `protocol`: String 23 | public var name: String? 24 | public var host: String 25 | public var port: Int 26 | 27 | public internal(set) var uid: UInt64? 28 | 29 | public init(protocol: String, host: String, port: Int, uid: UInt64?) { 30 | self.protocol = `protocol` 31 | self.name = nil 32 | self.host = host 33 | self.port = port 34 | self.uid = uid 35 | } 36 | 37 | public init(protocol: String, name: String?, host: String, port: Int, uid: UInt64?) { 38 | self.protocol = `protocol` 39 | if let name = name, name.isEmpty { 40 | self.name = nil 41 | } else { 42 | self.name = name 43 | } 44 | self.host = host 45 | self.port = port 46 | self.uid = uid 47 | } 48 | 49 | public var withoutUID: Self { 50 | var without = self 51 | without.uid = nil 52 | return without 53 | } 54 | 55 | public var description: String { 56 | // /// uid is not printed by default since we only care about it when we do, not in every place where we log a node 57 | // "\(self.protocol)://\(self.host):\(self.port)" 58 | self.detailedDescription 59 | } 60 | 61 | /// Prints a node's String representation including its `uid`. 62 | public var detailedDescription: String { 63 | "\(self.protocol)://\(self.name.map { "\($0)@" } ?? "")\(self.host):\(self.port)\(self.uid.map { "#\($0.description)" } ?? "")" 64 | } 65 | } 66 | 67 | extension Node { 68 | // Silly but good enough comparison for deciding "who is lower node" 69 | // as we only use those for "tie-breakers" any ordering is fine to be honest here. 70 | public static func < (lhs: Node, rhs: Node) -> Bool { 71 | if lhs.protocol == rhs.protocol, lhs.host == rhs.host { 72 | if lhs.port == rhs.port { 73 | return (lhs.uid ?? 0) < (rhs.uid ?? 0) 74 | } else { 75 | return lhs.port < rhs.port 76 | } 77 | } else { 78 | // "silly" but good enough comparison, we just need a predictable order, does not really matter what it is 79 | return "\(lhs.protocol)\(lhs.host)" < "\(rhs.protocol)\(rhs.host)" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/SWIM/Docs.docc/images/ping_pingreq_cycle.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/swift-cluster-membership/f5c6877dd7fd29f85adefcdd1173ba88fd756e9a/Sources/SWIM/Docs.docc/images/ping_pingreq_cycle.graffle -------------------------------------------------------------------------------- /Sources/SWIM/Docs.docc/images/ping_pingreq_cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/swift-cluster-membership/f5c6877dd7fd29f85adefcdd1173ba88fd756e9a/Sources/SWIM/Docs.docc/images/ping_pingreq_cycle.png -------------------------------------------------------------------------------- /Sources/SWIM/Docs.docc/images/swim_lifecycle.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/swift-cluster-membership/f5c6877dd7fd29f85adefcdd1173ba88fd756e9a/Sources/SWIM/Docs.docc/images/swim_lifecycle.graffle -------------------------------------------------------------------------------- /Sources/SWIM/Docs.docc/images/swim_lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/swift-cluster-membership/f5c6877dd7fd29f85adefcdd1173ba88fd756e9a/Sources/SWIM/Docs.docc/images/swim_lifecycle.png -------------------------------------------------------------------------------- /Sources/SWIM/Events.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | 17 | extension SWIM { 18 | /// Emitted whenever a membership change happens. 19 | /// 20 | /// Use `isReachabilityChange` to detect whether the is a change from an alive to unreachable/dead state or not, 21 | /// and is worth emitting to user-code or not. 22 | public struct MemberStatusChangedEvent: Equatable { 23 | /// The member that this change event is about. 24 | public let member: SWIM.Member 25 | 26 | /// The resulting ("current") status of the `member`. 27 | public var status: SWIM.Status { 28 | // Note if the member is marked .dead, SWIM shall continue to gossip about it for a while 29 | // such that other nodes gain this information directly, and do not have to wait until they detect 30 | // it as such independently. 31 | self.member.status 32 | } 33 | 34 | /// Previous status of the member, needed in order to decide if the change is "effective" or if applying the 35 | /// member did not move it in such way that we need to inform the cluster about unreachability. 36 | public let previousStatus: SWIM.Status? 37 | 38 | /// Create new event, representing a change of the member's status from a previous state to its current state. 39 | public init(previousStatus: SWIM.Status?, member: SWIM.Member) { 40 | if let from = previousStatus, from == .dead { 41 | precondition( 42 | member.status == .dead, 43 | "Change MUST NOT move status 'backwards' from [.dead] state to anything else, but did so, was: \(member)" 44 | ) 45 | } 46 | 47 | self.previousStatus = previousStatus 48 | self.member = member 49 | 50 | switch (self.previousStatus, member.status) { 51 | case (.dead, .alive), 52 | (.dead, .suspect), 53 | (.dead, .unreachable): 54 | fatalError( 55 | "SWIM.Membership MUST NOT move status 'backwards' from .dead state to anything else, but did so, was: \(self)" 56 | ) 57 | default: 58 | () // ok, all other transitions are valid. 59 | } 60 | } 61 | } 62 | } 63 | 64 | extension SWIM.MemberStatusChangedEvent { 65 | /// Reachability changes are important events, in which a reachable node became unreachable, or vice-versa, 66 | /// as opposed to events which only move a member between `.alive` and `.suspect` status, 67 | /// during which the member should still be considered and no actions assuming it's death shall be performed (yet). 68 | /// 69 | /// If true, a system may want to issue a reachability change event and handle this situation by confirming the node `.dead`, 70 | /// and proceeding with its removal from the cluster. 71 | public var isReachabilityChange: Bool { 72 | guard let fromStatus = self.previousStatus else { 73 | // i.e. nil -> anything, is always an effective reachability affecting change 74 | return true 75 | } 76 | 77 | // explicitly list all changes which are affecting reachability, all others do not (i.e. flipping between 78 | // alive and suspect does NOT affect high-level reachability). 79 | switch (fromStatus, self.status) { 80 | case (.alive, .unreachable), 81 | (.alive, .dead): 82 | return true 83 | case (.suspect, .unreachable), 84 | (.suspect, .dead): 85 | return true 86 | case (.unreachable, .alive), 87 | (.unreachable, .suspect): 88 | return true 89 | default: 90 | return false 91 | } 92 | } 93 | } 94 | 95 | extension SWIM.MemberStatusChangedEvent: CustomStringConvertible { 96 | public var description: String { 97 | var res = "MemberStatusChangedEvent(\(self.member), previousStatus: " 98 | if let previousStatus = self.previousStatus { 99 | res += "\(previousStatus)" 100 | } else { 101 | res += "" 102 | } 103 | res += ")" 104 | return res 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/SWIM/Member.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | 17 | @preconcurrency import struct Dispatch.DispatchTime 18 | 19 | // ==== ---------------------------------------------------------------------------------------------------------------- 20 | // MARK: SWIM Member 21 | 22 | extension SWIM { 23 | /// A `SWIM.Member` represents an active participant of the cluster. 24 | /// 25 | /// It associates a specific `SWIMAddressablePeer` with its `SWIM.Status` and a number of other SWIM specific state information. 26 | public struct Member: Sendable { 27 | /// Peer reference, used to send messages to this cluster member. 28 | /// 29 | /// Can represent the "local" member as well, use `swim.isMyself` to verify if a peer is `myself`. 30 | public var peer: Peer 31 | 32 | /// `Node` of the member's `peer`. 33 | public var node: ClusterMembership.Node { 34 | self.peer.node 35 | } 36 | 37 | /// Membership status of this cluster member 38 | public var status: SWIM.Status 39 | 40 | // Period in which protocol period was this state set 41 | public var protocolPeriod: UInt64 42 | 43 | /// Indicates a _local_ point in time when suspicion was started. 44 | /// 45 | /// - Note: Only suspect members may have this value set, but having the actual field in SWIM.Member feels more natural. 46 | /// - Note: This value is never carried across processes, as it serves only locally triggering suspicion timeouts. 47 | public let localSuspicionStartedAt: DispatchTime? // could be "status updated at"? 48 | 49 | /// Create a new member. 50 | public init(peer: Peer, status: SWIM.Status, protocolPeriod: UInt64, suspicionStartedAt: DispatchTime? = nil) { 51 | self.peer = peer 52 | self.status = status 53 | self.protocolPeriod = protocolPeriod 54 | self.localSuspicionStartedAt = suspicionStartedAt 55 | } 56 | 57 | /// Convenience function for checking if a member is `SWIM.Status.alive`. 58 | /// 59 | /// - Returns: `true` if the member is alive 60 | public var isAlive: Bool { 61 | self.status.isAlive 62 | } 63 | 64 | /// Convenience function for checking if a member is `SWIM.Status.suspect`. 65 | /// 66 | /// - Returns: `true` if the member is suspect 67 | public var isSuspect: Bool { 68 | self.status.isSuspect 69 | } 70 | 71 | /// Convenience function for checking if a member is `SWIM.Status.unreachable` 72 | /// 73 | /// - Returns: `true` if the member is unreachable 74 | public var isUnreachable: Bool { 75 | self.status.isUnreachable 76 | } 77 | 78 | /// Convenience function for checking if a member is `SWIM.Status.dead` 79 | /// 80 | /// - Returns: `true` if the member is dead 81 | public var isDead: Bool { 82 | self.status.isDead 83 | } 84 | } 85 | } 86 | 87 | /// Manual Hashable conformance since we omit `suspicionStartedAt` from identity 88 | extension SWIM.Member: Hashable, Equatable { 89 | public static func == (lhs: SWIM.Member, rhs: SWIM.Member) -> Bool { 90 | lhs.peer.node == rhs.peer.node && lhs.protocolPeriod == rhs.protocolPeriod && lhs.status == rhs.status 91 | } 92 | 93 | public func hash(into hasher: inout Hasher) { 94 | hasher.combine(self.peer.node) 95 | hasher.combine(self.protocolPeriod) 96 | hasher.combine(self.status) 97 | } 98 | } 99 | 100 | extension SWIM.Member: CustomStringConvertible, CustomDebugStringConvertible { 101 | public var description: String { 102 | var res = "SWIM.Member(\(self.peer), \(self.status), protocolPeriod: \(self.protocolPeriod)" 103 | if let suspicionStartedAt = self.localSuspicionStartedAt { 104 | res.append(", suspicionStartedAt: \(suspicionStartedAt)") 105 | } 106 | res.append(")") 107 | return res 108 | } 109 | 110 | public var debugDescription: String { 111 | var res = "SWIM.Member(\(String(reflecting: self.peer)), \(self.status), protocolPeriod: \(self.protocolPeriod)" 112 | if let suspicionStartedAt = self.localSuspicionStartedAt { 113 | res.append(", suspicionStartedAt: \(suspicionStartedAt)") 114 | } 115 | res.append(")") 116 | return res 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/SWIM/Metrics.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Metrics 16 | 17 | extension SWIM { 18 | /// Object containing all metrics a SWIM instance and shell should be reporting. 19 | /// 20 | /// - SeeAlso: `SWIM.Metrics.Shell` for metrics that a specific implementation should emit 21 | public struct Metrics { 22 | // ==== -------------------------------------------------------------------------------------------------------- 23 | // MARK: Membership 24 | 25 | /// Number of members (alive) 26 | public let membersAlive: Gauge 27 | /// Number of members (suspect) 28 | public let membersSuspect: Gauge 29 | /// Number of members (unreachable) 30 | public let membersUnreachable: Gauge 31 | // Number of members (dead) is not reported, because "dead" is considered "removed" from the cluster 32 | // -- no metric -- 33 | 34 | /// Total number of nodes *ever* declared noticed as dead by this member 35 | public let membersTotalDead: Counter 36 | 37 | /// The current number of tombstones for previously known (and now dead and removed) members. 38 | public let removedDeadMemberTombstones: Gauge 39 | 40 | // ==== -------------------------------------------------------------------------------------------------------- 41 | // MARK: Internal metrics 42 | 43 | /// Current value of the local health multiplier. 44 | public let localHealthMultiplier: Gauge 45 | 46 | // ==== -------------------------------------------------------------------------------------------------------- 47 | // MARK: Probe metrics 48 | 49 | /// Records the incarnation of the SWIM instance. 50 | /// 51 | /// Incarnation numbers are bumped whenever the node needs to refute some gossip about itself, 52 | /// as such the incarnation number *growth* is an interesting indicator of cluster observation churn. 53 | public let incarnation: Gauge 54 | 55 | /// Total number of successful probes (pings with successful replies) 56 | public let successfulPingProbes: Counter 57 | /// Total number of failed probes (pings with successful replies) 58 | public let failedPingProbes: Counter 59 | 60 | /// Total number of successful ping request probes (pingRequest with successful replies) 61 | /// Either an .ack or .nack from the intermediary node count as an success here 62 | public let successfulPingRequestProbes: Counter 63 | /// Total number of failed ping request probes (pings requests with successful replies) 64 | /// Only a .timeout counts as a failed ping request. 65 | public let failedPingRequestProbes: Counter 66 | 67 | // ==== ---------------------------------------------------------------------------------------------------------------- 68 | // MARK: Shell / Transport Metrics 69 | 70 | /// Metrics to be filled in by respective SWIM shell implementations. 71 | public let shell: ShellMetrics 72 | 73 | public struct ShellMetrics { 74 | // ==== ---------------------------------------------------------------------------------------------------- 75 | // MARK: Probe metrics 76 | 77 | /// Records time it takes for ping successful round-trips. 78 | public let pingResponseTime: Timer 79 | 80 | /// Records time it takes for (every) successful pingRequest round-trip 81 | public let pingRequestResponseTimeAll: Timer 82 | /// Records the time it takes for the (first) successful pingRequest to round trip 83 | /// (A ping request hits multiple intermediary peers, the first reply is what counts) 84 | public let pingRequestResponseTimeFirst: Timer 85 | 86 | /// Number of incoming messages received 87 | public let messageInboundCount: Counter 88 | /// Sizes of messages received, in bytes 89 | public let messageInboundBytes: Recorder 90 | 91 | /// Number of messages sent 92 | public let messageOutboundCount: Counter 93 | /// Sizes of messages sent, in bytes 94 | public let messageOutboundBytes: Recorder 95 | 96 | public init(settings: SWIM.Settings) { 97 | self.pingResponseTime = Timer( 98 | label: settings.metrics.makeLabel("roundTripTime", "ping") 99 | ) 100 | 101 | self.pingRequestResponseTimeAll = Timer( 102 | label: settings.metrics.makeLabel("roundTripTime", "pingRequest"), 103 | dimensions: [("type", "all")] 104 | ) 105 | self.pingRequestResponseTimeFirst = Timer( 106 | label: settings.metrics.makeLabel("roundTripTime", "pingRequest"), 107 | dimensions: [("type", "firstAck")] 108 | ) 109 | 110 | self.messageInboundCount = Counter( 111 | label: settings.metrics.makeLabel("message", "count"), 112 | dimensions: [ 113 | ("direction", "in") 114 | ] 115 | ) 116 | self.messageInboundBytes = Recorder( 117 | label: settings.metrics.makeLabel("message", "bytes"), 118 | dimensions: [ 119 | ("direction", "in") 120 | ] 121 | ) 122 | 123 | self.messageOutboundCount = Counter( 124 | label: settings.metrics.makeLabel("message", "count"), 125 | dimensions: [ 126 | ("direction", "out") 127 | ] 128 | ) 129 | self.messageOutboundBytes = Recorder( 130 | label: settings.metrics.makeLabel("message", "bytes"), 131 | dimensions: [ 132 | ("direction", "out") 133 | ] 134 | ) 135 | } 136 | } 137 | 138 | public init(settings: SWIM.Settings) { 139 | self.membersAlive = Gauge( 140 | label: settings.metrics.makeLabel("members"), 141 | dimensions: [("status", "alive")] 142 | ) 143 | self.membersSuspect = Gauge( 144 | label: settings.metrics.makeLabel("members"), 145 | dimensions: [("status", "suspect")] 146 | ) 147 | self.membersUnreachable = Gauge( 148 | label: settings.metrics.makeLabel("members"), 149 | dimensions: [("status", "unreachable")] 150 | ) 151 | self.membersTotalDead = Counter( 152 | label: settings.metrics.makeLabel("members", "total"), 153 | dimensions: [("status", "dead")] 154 | ) 155 | self.removedDeadMemberTombstones = Gauge( 156 | label: settings.metrics.makeLabel("removedMemberTombstones") 157 | ) 158 | 159 | self.localHealthMultiplier = Gauge( 160 | label: settings.metrics.makeLabel("lha") 161 | ) 162 | 163 | self.incarnation = Gauge(label: settings.metrics.makeLabel("incarnation")) 164 | 165 | self.successfulPingProbes = Counter( 166 | label: settings.metrics.makeLabel("probe", "ping"), 167 | dimensions: [("type", "successful")] 168 | ) 169 | self.failedPingProbes = Counter( 170 | label: settings.metrics.makeLabel("probe", "ping"), 171 | dimensions: [("type", "failed")] 172 | ) 173 | 174 | self.successfulPingRequestProbes = Counter( 175 | label: settings.metrics.makeLabel("probe", "pingRequest"), 176 | dimensions: [("type", "successful")] 177 | ) 178 | self.failedPingRequestProbes = Counter( 179 | label: settings.metrics.makeLabel("probe", "pingRequest"), 180 | dimensions: [("type", "failed")] 181 | ) 182 | 183 | self.shell = .init(settings: settings) 184 | } 185 | } 186 | } 187 | 188 | extension SWIM.Metrics { 189 | /// Update member metrics metrics based on SWIM's membership. 190 | public func updateMembership(_ members: SWIM.Membership) { 191 | var alives = 0 192 | var suspects = 0 193 | var unreachables = 0 194 | for member in members { 195 | switch member.status { 196 | case .alive: 197 | alives += 1 198 | case .suspect: 199 | suspects += 1 200 | case .unreachable: 201 | unreachables += 1 202 | case .dead: 203 | () // dead is reported as a removal when they're removed and tombstoned, not as a gauge 204 | } 205 | } 206 | self.membersAlive.record(alives) 207 | self.membersSuspect.record(suspects) 208 | self.membersUnreachable.record(unreachables) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Sources/SWIM/Peer.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020-2022 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | 17 | /// Any peer in the cluster, can be used used to identify a peer using its unique node that it represents. 18 | public protocol SWIMAddressablePeer: Sendable { 19 | /// Node that this peer is representing. 20 | nonisolated var swimNode: ClusterMembership.Node { get } 21 | } 22 | 23 | extension SWIMAddressablePeer { 24 | internal var node: ClusterMembership.Node { 25 | self.swimNode 26 | } 27 | } 28 | 29 | /// SWIM A peer which originated a `ping`, should be replied to with an `ack`. 30 | public protocol SWIMPingOriginPeer: SWIMAddressablePeer { 31 | associatedtype Peer: SWIMPeer 32 | 33 | /// Acknowledge a `ping`. 34 | /// 35 | /// - parameters: 36 | /// - sequenceNumber: the sequence number of the incoming ping that this ack should acknowledge 37 | /// - target: target peer which received the ping (i.e. "myself" on the recipient of the `ping`). 38 | /// - incarnation: incarnation number of the target (myself), 39 | /// which is used to clarify which status is the most recent on the recipient of this acknowledgement. 40 | /// - payload: additional gossip data to be carried with the message. 41 | /// It is already trimmed to be no larger than configured in `SWIM.Settings`. 42 | func ack( 43 | acknowledging sequenceNumber: SWIM.SequenceNumber, 44 | target: Peer, 45 | incarnation: SWIM.Incarnation, 46 | payload: SWIM.GossipPayload 47 | ) async throws 48 | } 49 | 50 | /// A SWIM peer which originated a `pingRequest` and thus can receive either an `ack` or `nack` from the intermediary. 51 | public protocol SWIMPingRequestOriginPeer: SWIMPingOriginPeer { 52 | associatedtype NackTarget: SWIMPeer 53 | 54 | /// "Negative acknowledge" a ping. 55 | /// 56 | /// This message may ONLY be send in an indirect-ping scenario from the "middle" peer. 57 | /// Meaning, only a peer which received a `pingRequest` and wants to send the `pingRequestOrigin` 58 | /// a nack in order for it to be aware that its message did reach this member, even if it never gets an `ack` 59 | /// through this member, e.g. since the pings `target` node is actually not reachable anymore. 60 | /// 61 | /// - parameters: 62 | /// - sequenceNumber: the sequence number of the incoming `pingRequest` that this nack is a response to 63 | /// - target: the target peer which was attempted to be pinged but we didn't get an ack from it yet and are sending a nack back eagerly 64 | func nack( 65 | acknowledging sequenceNumber: SWIM.SequenceNumber, 66 | target: NackTarget 67 | ) async throws 68 | } 69 | 70 | /// SWIM peer which can be initiated contact with, by sending ping or ping request messages. 71 | public protocol SWIMPeer: SWIMAddressablePeer { 72 | associatedtype Peer: SWIMPeer 73 | associatedtype PingOrigin: SWIMPingOriginPeer 74 | associatedtype PingRequestOrigin: SWIMPingRequestOriginPeer 75 | 76 | /// Perform a probe of this peer by sending a `ping` message. 77 | /// 78 | /// We expect the reply to be an `ack`. 79 | /// 80 | /// - parameters: 81 | /// - payload: additional gossip information to be processed by the recipient 82 | /// - origin: the origin peer that has initiated this ping message (i.e. "myself" of the sender) 83 | /// replies (`ack`s) from to this ping should be send to this peer 84 | /// - timeout: timeout during which we expect the other peer to have replied to us with a `PingResponse` about the pinged node. 85 | /// If we get no response about that peer in that time, this `ping` is considered failed, and the onResponse MUST be invoked with a `.timeout`. 86 | /// - sequenceNumber: sequence number of the ping message 87 | /// 88 | /// - Returns the corresponding reply (`ack`) or `timeout` event for this ping request occurs. 89 | /// 90 | /// - Throws if the ping fails or if the reply is `nack`. 91 | func ping( 92 | payload: SWIM.GossipPayload, 93 | from origin: PingOrigin, 94 | timeout: Duration, 95 | sequenceNumber: SWIM.SequenceNumber 96 | ) async throws -> SWIM.PingResponse 97 | 98 | /// Send a ping request to this peer, asking it to perform an "indirect ping" of the target on our behalf. 99 | /// 100 | /// Any resulting acknowledgements back to us. If not acknowledgements come back from the target, the intermediary 101 | /// may send back nack messages, indicating that our connection to the intermediary is intact, however we didn't see 102 | /// acknowledgements from the target itself. 103 | /// 104 | /// - parameters: 105 | /// - target: target peer that should be probed by this the recipient on our behalf 106 | /// - payload: additional gossip information to be processed by the recipient 107 | /// - origin: the origin peer that has initiated this `pingRequest` (i.e. "myself" on the sender); 108 | /// replies (`ack`s) from this indirect ping should be forwarded to it. 109 | /// - timeout: timeout during which we expect the other peer to have replied to us with a `PingResponse` about the pinged node. 110 | /// If we get no response about that peer in that time, this `pingRequest` is considered failed, and the onResponse MUST be invoked with a `.timeout`. 111 | /// - sequenceNumber: sequence number of the pingRequest message 112 | /// 113 | /// - Returns the corresponding reply (`ack`, `nack`) or `timeout` event for this ping request occurs. 114 | /// - Throws if the ping request fails 115 | func pingRequest( 116 | target: Peer, 117 | payload: SWIM.GossipPayload, 118 | from origin: PingOrigin, 119 | timeout: Duration, 120 | sequenceNumber: SWIM.SequenceNumber 121 | ) async throws -> SWIM.PingResponse 122 | } 123 | -------------------------------------------------------------------------------- /Sources/SWIM/SWIM.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | 17 | import struct Dispatch.DispatchTime 18 | 19 | extension SWIM { 20 | /// Incarnation numbers serve as sequence number and used to determine which observation 21 | /// is "more recent" when comparing gossiped information. 22 | public typealias Incarnation = UInt64 23 | 24 | /// A sequence number which can be used to associate with messages in order to establish an request/response 25 | /// relationship between ping/pingRequest and their corresponding ack/nack messages. 26 | public typealias SequenceNumber = UInt32 27 | 28 | /// Typealias for the underlying membership representation. 29 | public typealias Membership = Dictionary>.Values 30 | } 31 | 32 | extension SWIM { 33 | /// Message sent in reply to a `.ping`. 34 | /// 35 | /// The ack may be delivered directly in a request-response fashion between the probing and pinged members, 36 | /// or indirectly, as a result of a `pingRequest` message. 37 | public enum PingResponse: Sendable { 38 | /// - parameters: 39 | /// - target: the target of the ping; 40 | /// On the remote "pinged" node which is about to send an ack back to the ping origin this should be filled with the `myself` peer. 41 | /// - incarnation: the incarnation of the peer sent in the `target` field 42 | /// - payload: additional gossip data to be carried with the message. 43 | /// - sequenceNumber: the `sequenceNumber` of the `ping` message this ack is a "reply" for; 44 | /// It is used on the ping origin to co-relate the reply with its handling code. 45 | case ack( 46 | target: Peer, 47 | incarnation: Incarnation, 48 | payload: GossipPayload, 49 | sequenceNumber: SWIM.SequenceNumber 50 | ) 51 | 52 | /// A `.nack` MAY ONLY be sent by an *intermediary* member which was received a `pingRequest` to perform a `ping` of some `target` member. 53 | /// It SHOULD NOT be sent by a peer that received a `.ping` directly. 54 | /// 55 | /// The nack allows the origin of the ping request to know if the `k` peers it asked to perform the indirect probes, 56 | /// are still responsive to it, or if perhaps that communication by itself is also breaking down. This information is 57 | /// used to adjust the `localHealthMultiplier`, which impacts probe and timeout intervals. 58 | /// 59 | /// Note that nack information DOES NOT directly cause unreachability or suspicions, it only adjusts the timeouts 60 | /// and intervals used by the swim instance in order to take into account the potential that our local node is 61 | /// potentially not healthy. 62 | /// 63 | /// - parameters: 64 | /// - target: the target of the ping; 65 | /// On the remote "pinged" node which is about to send an ack back to the ping origin this should be filled with the `myself` peer. 66 | /// - target: the target of the ping; 67 | /// On the remote "pinged" node which is about to send an ack back to the ping origin this should be filled with the `myself` peer. 68 | /// - payload: The gossip payload to be carried in this message. 69 | /// 70 | /// - SeeAlso: Lifeguard IV.A. Local Health Aware Probe 71 | case nack(target: Peer, sequenceNumber: SWIM.SequenceNumber) 72 | 73 | /// This is a "pseudo-message", in the sense that it is not transported over the wire, but should be triggered 74 | /// and fired into an implementation Shell when a ping has timed out. 75 | /// 76 | /// If a response for some reason produces a different error immediately rather than through a timeout, 77 | /// the shell should also emit a `.timeout` response and feed it into the `SWIM.Instance` as it is important for 78 | /// timeout adjustments that the instance makes. The instance does not need to know specifics about the reason of 79 | /// a response not arriving, thus they are all handled via the same timeout response rather than extra "error" responses. 80 | /// 81 | /// - parameters: 82 | /// - target: the target of the ping; 83 | /// On the remote "pinged" node which is about to send an ack back to the ping origin this should be filled with the `myself` peer. 84 | /// - pingRequestOrigin: if this response/timeout is in response to a ping that was caused by a pingRequest, 85 | /// `pingRequestOrigin` must contain the original peer which originated the ping request. 86 | /// - timeout: the timeout interval value that caused this message to be triggered; 87 | /// In case of "cancelled" operations or similar semantics it is allowed to use a placeholder value here. 88 | /// - sequenceNumber: the `sequenceNumber` of the `ping` message this ack is a "reply" for; 89 | /// It is used on the ping origin to co-relate the reply with its handling code. 90 | case timeout( 91 | target: Peer, 92 | pingRequestOrigin: PingRequestOrigin?, 93 | timeout: Duration, 94 | sequenceNumber: SWIM.SequenceNumber 95 | ) 96 | 97 | /// Sequence number of the initial request this is a response to. 98 | /// Used to pair up responses to the requests which initially caused them. 99 | /// 100 | /// All ping responses are guaranteed to have a sequence number attached to them. 101 | public var sequenceNumber: SWIM.SequenceNumber { 102 | switch self { 103 | case .ack(_, _, _, let sequenceNumber): 104 | return sequenceNumber 105 | case .nack(_, let sequenceNumber): 106 | return sequenceNumber 107 | case .timeout(_, _, _, let sequenceNumber): 108 | return sequenceNumber 109 | } 110 | } 111 | } 112 | } 113 | 114 | // ==== ---------------------------------------------------------------------------------------------------------------- 115 | // MARK: Gossip 116 | 117 | extension SWIM { 118 | /// A piece of "gossip" about a specific member of the cluster. 119 | /// 120 | /// A gossip will only be spread a limited number of times, as configured by `settings.gossip.gossipedEnoughTimes(_:members:)`. 121 | public struct Gossip: Equatable { 122 | /// The specific member (including status) that this gossip is about. 123 | /// 124 | /// A change in member status implies a new gossip must be created and the count for the rumor mongering must be reset. 125 | public let member: SWIM.Member 126 | /// The number of times this specific gossip message was gossiped to another peer. 127 | public internal(set) var numberOfTimesGossiped: Int 128 | } 129 | 130 | /// A `GossipPayload` is used to spread gossips about members. 131 | public enum GossipPayload: Sendable { 132 | /// Explicit case to signal "no gossip payload" 133 | /// 134 | /// Effectively equivalent to an empty `.membership([])` case. 135 | case none 136 | /// Gossip information about a few select members. 137 | case membership([SWIM.Member]) 138 | } 139 | } 140 | 141 | extension SWIM.GossipPayload { 142 | /// True if the underlying gossip is empty. 143 | public var isNone: Bool { 144 | switch self { 145 | case .none: 146 | return true 147 | case .membership: 148 | return false 149 | } 150 | } 151 | 152 | /// True if the underlying gossip contains membership information. 153 | public var isMembership: Bool { 154 | switch self { 155 | case .none: 156 | return false 157 | case .membership: 158 | return true 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Sources/SWIM/Status.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2018 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | 17 | extension SWIM { 18 | /// The SWIM membership status reflects how a node is perceived by the distributed failure detector. 19 | /// 20 | /// ### Modification: Unreachable status (opt-in) 21 | /// If the unreachable status extension is enabled, it is set / when a classic SWIM implementation would have 22 | /// declared a node `.dead`, / yet since we allow for the higher level membership to decide when and how to eject 23 | /// members from a cluster, / only the `.unreachable` state is set and an `Cluster.ReachabilityChange` cluster event 24 | /// is emitted. / In response to this a high-level membership protocol MAY confirm the node as dead by issuing 25 | /// `Instance.confirmDead`, / which will promote the node to `.dead` in SWIM terms. 26 | /// 27 | /// > The additional `.unreachable` status is only used it enabled explicitly by setting `settings.unreachable` 28 | /// > to enabled. Otherwise, the implementation performs its failure checking as usual and directly marks detected 29 | /// > to be failed members as `.dead`. 30 | /// 31 | /// ### Legal transitions: 32 | /// - `alive -> suspect` 33 | /// - `alive -> suspect`, with next `SWIM.Incarnation`, e.g. during flaky network situations, we suspect and un-suspect a node depending on probing 34 | /// - `suspect -> unreachable | alive`, if in SWIM terms, a node is "most likely dead" we declare it `.unreachable` instead, and await for high-level confirmation to mark it `.dead`. 35 | /// - `unreachable -> alive | suspect`, with next `SWIM.Incarnation` optional) 36 | /// - `alive | suspect | unreachable -> dead` 37 | /// 38 | /// - SeeAlso: `SWIM.Incarnation` 39 | public enum Status: Hashable, Sendable { 40 | /// Indicates an `alive` member of the cluster, i.e. if is reachable and properly replies to all probes on time. 41 | case alive(incarnation: Incarnation) 42 | /// Indicates a `suspect` member of the cluster, meaning that it did not reply on time to probing and MAY be unreachable. 43 | /// Further probing and indirect probing will be performed to test if it really is unreachable/dead, 44 | /// or just had a small glitch (or network issues). 45 | case suspect(incarnation: Incarnation, suspectedBy: Set) 46 | /// Extension from traditional SWIM states: indicates an unreachable node, under traditional SWIM it would have 47 | /// already been marked `.dead`, however unreachability allows for a final extra step including a `swim.confirmDead()` 48 | /// call, to move the unreachable node to dead state. 49 | /// 50 | /// This only matters for multi layer membership protocols which use SWIM as their failure detection mechanism. 51 | /// 52 | /// This state is DISABLED BY DEFAULT, and if a node receives such unreachable status about another member while 53 | /// this setting is disabled it will immediately treat such member as `.dead`. Do not run in mixed mode clusters, 54 | /// as this can yield unexpected consequences. 55 | case unreachable(incarnation: Incarnation) 56 | /// Indicates 57 | /// Note: In the original paper this state was referred to as "confirm", which we found slightly confusing, thus the rename. 58 | case dead 59 | } 60 | } 61 | 62 | extension SWIM.Status: Comparable { 63 | public static func < (lhs: SWIM.Status, rhs: SWIM.Status) -> Bool { 64 | switch (lhs, rhs) { 65 | case (.alive(let selfIncarnation), .alive(let rhsIncarnation)): 66 | return selfIncarnation < rhsIncarnation 67 | case (.alive(let selfIncarnation), .suspect(let rhsIncarnation, _)): 68 | return selfIncarnation <= rhsIncarnation 69 | case (.alive(let selfIncarnation), .unreachable(let rhsIncarnation)): 70 | return selfIncarnation <= rhsIncarnation 71 | case (.suspect(let selfIncarnation, let selfSuspectedBy), .suspect(let rhsIncarnation, let rhsSuspectedBy)): 72 | return selfIncarnation < rhsIncarnation 73 | || (selfIncarnation == rhsIncarnation && selfSuspectedBy.isStrictSubset(of: rhsSuspectedBy)) 74 | case (.suspect(let selfIncarnation, _), .alive(let rhsIncarnation)): 75 | return selfIncarnation < rhsIncarnation 76 | case (.suspect(let selfIncarnation, _), .unreachable(let rhsIncarnation)): 77 | return selfIncarnation <= rhsIncarnation 78 | case (.unreachable(let selfIncarnation), .alive(let rhsIncarnation)): 79 | return selfIncarnation < rhsIncarnation 80 | case (.unreachable(let selfIncarnation), .suspect(let rhsIncarnation, _)): 81 | return selfIncarnation < rhsIncarnation 82 | case (.unreachable(let selfIncarnation), .unreachable(let rhsIncarnation)): 83 | return selfIncarnation < rhsIncarnation 84 | case (.dead, _): 85 | return false 86 | case (_, .dead): 87 | return true 88 | } 89 | } 90 | } 91 | 92 | extension SWIM.Status { 93 | /// Only `alive` or `suspect` members carry an incarnation number. 94 | public var incarnation: SWIM.Incarnation? { 95 | switch self { 96 | case .alive(let incarnation): 97 | return incarnation 98 | case .suspect(let incarnation, _): 99 | return incarnation 100 | case .unreachable(let incarnation): 101 | return incarnation 102 | case .dead: 103 | return nil 104 | } 105 | } 106 | 107 | /// - Returns: true if the underlying member status is `.alive`, false otherwise. 108 | public var isAlive: Bool { 109 | switch self { 110 | case .alive: 111 | return true 112 | case .suspect, .unreachable, .dead: 113 | return false 114 | } 115 | } 116 | 117 | /// - Returns: true if the underlying member status is `.suspect`, false otherwise. 118 | public var isSuspect: Bool { 119 | switch self { 120 | case .suspect: 121 | return true 122 | case .alive, .unreachable, .dead: 123 | return false 124 | } 125 | } 126 | 127 | /// - Returns: true if the underlying member status is `.unreachable`, false otherwise. 128 | public var isUnreachable: Bool { 129 | switch self { 130 | case .unreachable: 131 | return true 132 | case .alive, .suspect, .dead: 133 | return false 134 | } 135 | } 136 | 137 | /// - Returns: `true` if the underlying member status is `.unreachable`, false otherwise. 138 | public var isDead: Bool { 139 | switch self { 140 | case .dead: 141 | return true 142 | case .alive, .suspect, .unreachable: 143 | return false 144 | } 145 | } 146 | 147 | /// - Returns `true` if `self` is greater than or equal to `other` based on the 148 | /// following ordering: `alive(N)` < `suspect(N)` < `alive(N+1)` < `suspect(N+1)` < `dead` 149 | public func supersedes(_ other: SWIM.Status) -> Bool { 150 | self >= other 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/SWIM/Utils/Heap.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftNIO open source project 4 | // 5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.md for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | // Based on https://raw.githubusercontent.com/apple/swift-nio/bf2598d19359e43b4cfaffaff250986ebe677721/Sources/NIO/Heap.swift 16 | 17 | #if canImport(Darwin) 18 | import Darwin 19 | #elseif canImport(Glibc) 20 | import Glibc 21 | #elseif canImport(Musl) 22 | import Musl 23 | #else 24 | #error("Unsupported platform") 25 | #endif 26 | 27 | internal enum HeapType { 28 | case maxHeap 29 | case minHeap 30 | 31 | public func comparator(type: T.Type) -> (T, T) -> Bool { 32 | switch self { 33 | case .maxHeap: 34 | return (>) 35 | case .minHeap: 36 | return (<) 37 | } 38 | } 39 | } 40 | 41 | /// Slightly modified version of SwiftNIO's Heap, by exposing the comparator. 42 | internal struct Heap { 43 | internal private(set) var storage: ContiguousArray = [] 44 | private let comparator: (T, T) -> Bool 45 | 46 | init(of type: T.Type = T.self, comparator: @escaping (T, T) -> Bool) { 47 | self.comparator = comparator 48 | } 49 | 50 | // named `PARENT` in CLRS 51 | private func parentIndex(_ i: Int) -> Int { 52 | (i - 1) / 2 53 | } 54 | 55 | // named `LEFT` in CLRS 56 | private func leftIndex(_ i: Int) -> Int { 57 | 2 * i + 1 58 | } 59 | 60 | // named `RIGHT` in CLRS 61 | private func rightIndex(_ i: Int) -> Int { 62 | 2 * i + 2 63 | } 64 | 65 | // named `MAX-HEAPIFY` in CLRS 66 | private mutating func heapify(_ index: Int) { 67 | let left = self.leftIndex(index) 68 | let right = self.rightIndex(index) 69 | 70 | var root: Int 71 | if left <= (self.storage.count - 1), self.comparator(self.storage[left], self.storage[index]) { 72 | root = left 73 | } else { 74 | root = index 75 | } 76 | 77 | if right <= (self.storage.count - 1), self.comparator(self.storage[right], self.storage[root]) { 78 | root = right 79 | } 80 | 81 | if root != index { 82 | self.storage.swapAt(index, root) 83 | self.heapify(root) 84 | } 85 | } 86 | 87 | // named `HEAP-INCREASE-KEY` in CRLS 88 | private mutating func heapRootify(index: Int, key: T) { 89 | var index = index 90 | if self.comparator(self.storage[index], key) { 91 | fatalError("New key must be closer to the root than current key") 92 | } 93 | 94 | self.storage[index] = key 95 | while index > 0, self.comparator(self.storage[index], self.storage[self.parentIndex(index)]) { 96 | self.storage.swapAt(index, self.parentIndex(index)) 97 | index = self.parentIndex(index) 98 | } 99 | } 100 | 101 | public mutating func append(_ value: T) { 102 | var i = self.storage.count 103 | self.storage.append(value) 104 | while i > 0, self.comparator(self.storage[i], self.storage[self.parentIndex(i)]) { 105 | self.storage.swapAt(i, self.parentIndex(i)) 106 | i = self.parentIndex(i) 107 | } 108 | } 109 | 110 | @discardableResult 111 | public mutating func removeRoot() -> T? { 112 | self.remove(index: 0) 113 | } 114 | 115 | @discardableResult 116 | public mutating func remove(value: T) -> Bool { 117 | if let idx = self.storage.firstIndex(of: value) { 118 | self.remove(index: idx) 119 | return true 120 | } else { 121 | return false 122 | } 123 | } 124 | 125 | @discardableResult 126 | public mutating func remove(where: (T) throws -> Bool) rethrows -> Bool { 127 | if let idx = try self.storage.firstIndex(where: `where`) { 128 | self.remove(index: idx) 129 | return true 130 | } else { 131 | return false 132 | } 133 | } 134 | 135 | @discardableResult 136 | private mutating func remove(index: Int) -> T? { 137 | guard self.storage.count > 0 else { 138 | return nil 139 | } 140 | let element = self.storage[index] 141 | let comparator = self.comparator 142 | if self.storage.count == 1 || self.storage[index] == self.storage[self.storage.count - 1] { 143 | self.storage.removeLast() 144 | } else if !comparator(self.storage[index], self.storage[self.storage.count - 1]) { 145 | self.heapRootify(index: index, key: self.storage[self.storage.count - 1]) 146 | self.storage.removeLast() 147 | } else { 148 | self.storage[index] = self.storage[self.storage.count - 1] 149 | self.storage.removeLast() 150 | self.heapify(index) 151 | } 152 | return element 153 | } 154 | 155 | internal func checkHeapProperty() -> Bool { 156 | func checkHeapProperty(index: Int) -> Bool { 157 | let li = self.leftIndex(index) 158 | let ri = self.rightIndex(index) 159 | if index >= self.storage.count { 160 | return true 161 | } else { 162 | let me = self.storage[index] 163 | var lCond = true 164 | var rCond = true 165 | if li < self.storage.count { 166 | let l = self.storage[li] 167 | lCond = !self.comparator(l, me) 168 | } 169 | if ri < self.storage.count { 170 | let r = self.storage[ri] 171 | rCond = !self.comparator(r, me) 172 | } 173 | return lCond && rCond && checkHeapProperty(index: li) && checkHeapProperty(index: ri) 174 | } 175 | } 176 | return checkHeapProperty(index: 0) 177 | } 178 | } 179 | 180 | extension Heap: CustomDebugStringConvertible { 181 | var debugDescription: String { 182 | guard self.storage.count > 0 else { 183 | return "" 184 | } 185 | let descriptions = self.storage.map { String(describing: $0) } 186 | let maxLen: Int = descriptions.map { $0.count }.max()! // storage checked non-empty above 187 | let paddedDescs = descriptions.map { (desc: String) -> String in 188 | var desc = desc 189 | while desc.count < maxLen { 190 | if desc.count % 2 == 0 { 191 | desc = " \(desc)" 192 | } else { 193 | desc = "\(desc) " 194 | } 195 | } 196 | return desc 197 | } 198 | 199 | var all = "\n" 200 | let spacing = String(repeating: " ", count: maxLen) 201 | func subtreeWidths(rootIndex: Int) -> (Int, Int) { 202 | let lcIdx = self.leftIndex(rootIndex) 203 | let rcIdx = self.rightIndex(rootIndex) 204 | var leftSpace = 0 205 | var rightSpace = 0 206 | if lcIdx < self.storage.count { 207 | let sws = subtreeWidths(rootIndex: lcIdx) 208 | leftSpace += sws.0 + sws.1 + maxLen 209 | } 210 | if rcIdx < self.storage.count { 211 | let sws = subtreeWidths(rootIndex: rcIdx) 212 | rightSpace += sws.0 + sws.1 + maxLen 213 | } 214 | return (leftSpace, rightSpace) 215 | } 216 | for (index, desc) in paddedDescs.enumerated() { 217 | let (leftWidth, rightWidth) = subtreeWidths(rootIndex: index) 218 | all += String(repeating: " ", count: leftWidth) 219 | all += desc 220 | all += String(repeating: " ", count: rightWidth) 221 | 222 | func height(index: Int) -> Int { 223 | Int(log2(Double(index + 1))) 224 | } 225 | let myHeight = height(index: index) 226 | let nextHeight = height(index: index + 1) 227 | if myHeight != nextHeight { 228 | all += "\n" 229 | } else { 230 | all += spacing 231 | } 232 | } 233 | all += "\n" 234 | return all 235 | } 236 | } 237 | 238 | struct HeapIterator: IteratorProtocol { 239 | typealias Element = T 240 | 241 | private var heap: Heap 242 | 243 | init(heap: Heap) { 244 | self.heap = heap 245 | } 246 | 247 | mutating func next() -> T? { 248 | self.heap.removeRoot() 249 | } 250 | } 251 | 252 | extension Heap: Sequence { 253 | typealias Element = T 254 | 255 | var startIndex: Int { self.storage.startIndex } 256 | var endIndex: Int { self.storage.endIndex } 257 | 258 | var underestimatedCount: Int { 259 | self.storage.count 260 | } 261 | 262 | func makeIterator() -> HeapIterator { 263 | HeapIterator(heap: self) 264 | } 265 | 266 | subscript(position: Int) -> T { 267 | self.storage[position] 268 | } 269 | 270 | func index(after i: Int) -> Int { 271 | i + 1 272 | } 273 | 274 | // TODO: document if cheap (AFAICS yes) 275 | var count: Int { 276 | self.storage.count 277 | } 278 | } 279 | 280 | extension Heap where T: Comparable { 281 | init?(type: HeapType, storage: ContiguousArray) { 282 | self.comparator = type.comparator(type: T.self) 283 | self.storage = storage 284 | if !self.checkHeapProperty() { 285 | return nil 286 | } 287 | } 288 | 289 | init(type: HeapType) { 290 | self.comparator = type.comparator(type: T.self) 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /Sources/SWIM/Utils/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | // ==== ---------------------------------------------------------------------------------------------------------------- 16 | // MARK: String Interpolation: reflecting: 17 | 18 | extension String.StringInterpolation { 19 | mutating func appendInterpolation(reflecting subject: Any?) { 20 | self.appendLiteral(String(reflecting: subject)) 21 | } 22 | 23 | mutating func appendInterpolation(reflecting subject: Any) { 24 | self.appendLiteral(String(reflecting: subject)) 25 | } 26 | } 27 | 28 | // ==== ---------------------------------------------------------------------------------------------------------------- 29 | // MARK: String Interpolation: lineByLine: 30 | 31 | extension String.StringInterpolation { 32 | mutating func appendInterpolation(lineByLine subject: [Any]) { 33 | self.appendLiteral("\n \(subject.map { "\($0)" }.joined(separator: "\n "))") 34 | } 35 | } 36 | 37 | // ==== ---------------------------------------------------------------------------------------------------------------- 38 | // MARK: String Interpolation: _:orElse: 39 | 40 | extension String.StringInterpolation { 41 | mutating func appendInterpolation(_ value: T?, orElse defaultValue: String) { 42 | self.appendLiteral("\(value.map { "\($0)" } ?? defaultValue)") 43 | } 44 | 45 | mutating func appendInterpolation(optional value: T?) { 46 | self.appendLiteral("\(value.map { "\($0)" } ?? "nil")") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/SWIM/Utils/_PrettyLog.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Logging 16 | 17 | import struct Foundation.Calendar 18 | import struct Foundation.Date 19 | import class Foundation.DateFormatter 20 | import struct Foundation.Locale 21 | 22 | /// Pretty log formatter which prints log lines in the following multi line format, 23 | /// listing every metadata element in it's own, `//`-prefixed, line as well as pretty printing connections if set as `Logger.MetadataValue`. 24 | /// 25 | /// Example output: 26 | /// 27 | /// ``` 28 | /// SWIMNIOSample: [2020-08-25 0:7:59.8420] [info] [Example.swift:66] Membership status changed: [udp://127.0.0.1:7001#7015602685756068157] is now [alive(incarnation: 0)] 29 | //// metadata: 30 | //// "swim/member": udp://127.0.0.1:7001#7015602685756068157 31 | //// "swim/member/previousStatus": unknown 32 | //// "swim/member/status": alive(incarnation: 0) 33 | /// ``` 34 | /// 35 | /// Pro tip: you may want to use a coloring terminal application, which colors lines prefixed with `//` with a slightly different color, 36 | /// which makes visually parsing metadata vs. log message lines even more visually pleasing. 37 | public struct _SWIMPrettyMetadataLogHandler: LogHandler { 38 | let CONSOLE_RESET = "\u{001B}[0;0m" 39 | let CONSOLE_BOLD = "\u{001B}[1m" 40 | 41 | let label: String 42 | 43 | /// :nodoc: 44 | public init(_ label: String) { 45 | self.label = label 46 | } 47 | 48 | public subscript(metadataKey _: String) -> Logger.Metadata.Value? { 49 | get { 50 | [:] 51 | } 52 | set {} 53 | } 54 | 55 | public var metadata: Logger.Metadata = [:] 56 | public var logLevel: Logger.Level = .trace 57 | 58 | public func log( 59 | level: Logger.Level, 60 | message: Logger.Message, 61 | metadata: Logger.Metadata?, 62 | source: String, 63 | file: String, 64 | function: String, 65 | line: UInt 66 | ) { 67 | var metadataString: String = "" 68 | if let metadata = metadata { 69 | if !metadata.isEmpty { 70 | metadataString = "\n// metadata:\n" 71 | for key in metadata.keys.sorted() { 72 | let value: Logger.MetadataValue = metadata[key]! 73 | let valueDescription = self.prettyPrint(metadata: value) 74 | 75 | var allString = "\n// \"\(key)\": \(valueDescription)" 76 | if allString.contains("\n") { 77 | allString = String( 78 | allString.split(separator: "\n").map { valueLine in 79 | if valueLine.starts(with: "// ") { 80 | return "\(valueLine)\n" 81 | } else { 82 | return "// \(valueLine)\n" 83 | } 84 | }.joined(separator: "") 85 | ) 86 | } 87 | metadataString.append(allString) 88 | } 89 | metadataString = String(metadataString.dropLast(1)) 90 | } 91 | } 92 | let date = self._createFormatter().string(from: Date()) 93 | let file = file.split(separator: "/").last ?? "" 94 | let line = line 95 | print( 96 | "\(self.CONSOLE_BOLD)\(self.label)\(self.CONSOLE_RESET): [\(date)] [\(level)] [\(file):\(line)] \(message)\(metadataString)" 97 | ) 98 | } 99 | 100 | internal func prettyPrint(metadata: Logger.MetadataValue) -> String { 101 | var valueDescription = "" 102 | switch metadata { 103 | case .string(let string): 104 | valueDescription = string 105 | case .stringConvertible(let convertible): 106 | valueDescription = convertible.description 107 | case .array(let array): 108 | valueDescription = "\n \(array.map { "\($0)" }.joined(separator: "\n "))" 109 | case .dictionary(let metadata): 110 | for k in metadata.keys { 111 | valueDescription += "\(CONSOLE_BOLD)\(k)\(CONSOLE_RESET): \(self.prettyPrint(metadata: metadata[k]!))" 112 | } 113 | } 114 | 115 | return valueDescription 116 | } 117 | 118 | private func _createFormatter() -> DateFormatter { 119 | let formatter = DateFormatter() 120 | formatter.dateFormat = "y-MM-dd H:m:ss.SSSS" 121 | formatter.locale = Locale(identifier: "en_US") 122 | formatter.calendar = Calendar(identifier: .gregorian) 123 | return formatter 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/SWIM/Utils/time.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | extension Swift.Duration { 16 | typealias Value = Int64 17 | 18 | var nanoseconds: Value { 19 | let (seconds, attoseconds) = self.components 20 | let sNanos = seconds * Value(1_000_000_000) 21 | let asNanos = attoseconds / Value(1_000_000_000) 22 | let (totalNanos, overflow) = sNanos.addingReportingOverflow(asNanos) 23 | return overflow ? .max : totalNanos 24 | } 25 | 26 | /// The microseconds representation of the `TimeAmount`. 27 | var microseconds: Value { 28 | self.nanoseconds / TimeUnit.microseconds.rawValue 29 | } 30 | 31 | /// The milliseconds representation of the `TimeAmount`. 32 | var milliseconds: Value { 33 | self.nanoseconds / TimeUnit.milliseconds.rawValue 34 | } 35 | 36 | /// The seconds representation of the `TimeAmount`. 37 | var seconds: Value { 38 | self.nanoseconds / TimeUnit.seconds.rawValue 39 | } 40 | 41 | var isEffectivelyInfinite: Bool { 42 | self.nanoseconds == .max 43 | } 44 | 45 | /// Represents number of nanoseconds within given time unit 46 | enum TimeUnit: Value { 47 | case days = 86_400_000_000_000 48 | case hours = 3_600_000_000_000 49 | case minutes = 60_000_000_000 50 | case seconds = 1_000_000_000 51 | case milliseconds = 1_000_000 52 | case microseconds = 1000 53 | case nanoseconds = 1 54 | 55 | var abbreviated: String { 56 | switch self { 57 | case .nanoseconds: return "ns" 58 | case .microseconds: return "μs" 59 | case .milliseconds: return "ms" 60 | case .seconds: return "s" 61 | case .minutes: return "m" 62 | case .hours: return "h" 63 | case .days: return "d" 64 | } 65 | } 66 | 67 | func duration(_ duration: Int) -> Duration { 68 | switch self { 69 | case .nanoseconds: return .nanoseconds(Value(duration)) 70 | case .microseconds: return .microseconds(Value(duration)) 71 | case .milliseconds: return .milliseconds(Value(duration)) 72 | case .seconds: return .seconds(Value(duration)) 73 | case .minutes: return .seconds(Value(duration) * 60) 74 | case .hours: return .seconds(Value(duration) * 60 * 60) 75 | case .days: return .seconds(Value(duration) * 24 * 60 * 60) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/SWIMNIOExample/Logging.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import Logging 17 | import NIO 18 | import SWIM 19 | 20 | // ==== ---------------------------------------------------------------------------------------------------------------- 21 | // MARK: Tracelog: SWIM [tracelog:SWIM] 22 | 23 | extension SWIMNIOShell { 24 | /// Optional "dump all messages" logging. 25 | /// 26 | /// Enabled by `SWIM.Settings.traceLogLevel` or `-DTRACELOG_SWIM` 27 | func tracelog( 28 | _ type: TraceLogType, 29 | message: @autoclosure () -> String, 30 | file: String = #file, 31 | function: String = #function, 32 | line: UInt = #line 33 | ) { 34 | if let level = self.settings.swim.traceLogLevel { 35 | self.log.log( 36 | level: level, 37 | "[\(self.myself.node)] \(type.description) :: \(message())", 38 | metadata: self.swim.metadata, 39 | file: file, 40 | function: function, 41 | line: line 42 | ) 43 | } 44 | } 45 | 46 | internal enum TraceLogType: CustomStringConvertible { 47 | case send(to: SWIMAddressablePeer) 48 | case reply(to: SWIMAddressablePeer) 49 | case receive(pinged: SWIMAddressablePeer?) 50 | 51 | static var receive: TraceLogType { 52 | .receive(pinged: nil) 53 | } 54 | 55 | var description: String { 56 | switch self { 57 | case .send(let to): 58 | return "SEND(to:\(to.swimNode))" 59 | case .receive(nil): 60 | return "RECV" 61 | case .receive(let .some(pinged)): 62 | return "RECV(pinged:\(pinged.swimNode))" 63 | case .reply(let to): 64 | return "REPL(to:\(to.swimNode))" 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/SWIMNIOExample/Message.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import Logging 17 | import NIO 18 | import SWIM 19 | 20 | extension SWIM { 21 | public enum Message { 22 | case ping(replyTo: NIOPeer, payload: GossipPayload, sequenceNumber: SWIM.SequenceNumber) 23 | 24 | /// "Ping Request" requests a SWIM probe. 25 | case pingRequest( 26 | target: NIOPeer, 27 | replyTo: NIOPeer, 28 | payload: GossipPayload, 29 | sequenceNumber: SWIM.SequenceNumber 30 | ) 31 | 32 | case response(PingResponse) 33 | 34 | var messageCaseDescription: String { 35 | switch self { 36 | case .ping(_, _, let nr): 37 | return "ping@\(nr)" 38 | case .pingRequest(_, _, _, let nr): 39 | return "pingRequest@\(nr)" 40 | case .response(.ack(_, _, _, let nr)): 41 | return "response/ack@\(nr)" 42 | case .response(.nack(_, let nr)): 43 | return "response/nack@\(nr)" 44 | case .response(.timeout(_, _, _, let nr)): 45 | // not a "real message" 46 | return "response/timeout@\(nr)" 47 | } 48 | } 49 | 50 | /// Responses are special treated, i.e. they may trigger a pending completion closure 51 | var isResponse: Bool { 52 | switch self { 53 | case .response: 54 | return true 55 | default: 56 | return false 57 | } 58 | } 59 | 60 | var sequenceNumber: SWIM.SequenceNumber { 61 | switch self { 62 | case .ping(_, _, let sequenceNumber): 63 | return sequenceNumber 64 | case .pingRequest(_, _, _, let sequenceNumber): 65 | return sequenceNumber 66 | case .response(.ack(_, _, _, let sequenceNumber)): 67 | return sequenceNumber 68 | case .response(.nack(_, let sequenceNumber)): 69 | return sequenceNumber 70 | case .response(.timeout(_, _, _, let sequenceNumber)): 71 | return sequenceNumber 72 | } 73 | } 74 | } 75 | 76 | public enum LocalMessage { 77 | /// Sent by `ClusterShell` when wanting to join a cluster node by `Node`. 78 | /// 79 | /// Requests SWIM to monitor a node, which also causes an association to this node to be requested 80 | /// start gossiping SWIM messages with the node once established. 81 | case monitor(Node) 82 | 83 | /// Sent by `ClusterShell` whenever a `cluster.down(node:)` command is issued. 84 | /// 85 | /// ### Warning 86 | /// As both the `SWIMShell` or `ClusterShell` may play the role of origin of a command `cluster.down()`, 87 | /// it is important that the `SWIMShell` does NOT issue another `cluster.down()` once a member it already knows 88 | /// to be dead is `confirmDead`-ed again, as this would cause an infinite loop of the cluster and SWIM shells 89 | /// telling each other about the dead node. 90 | /// 91 | /// The intended interactions are: 92 | /// 1. user driven: 93 | /// - user issues `cluster.down(node)` 94 | /// - `ClusterShell` marks the node as `.down` immediately and notifies SWIM with `.confirmDead(node)` 95 | /// - `SWIMShell` updates its failure detection and gossip to mark the node as `.dead` 96 | /// - SWIM continues to gossip this `.dead` information to let other nodes know about this decision; 97 | /// * one case where it may not be able to do so is if the downed node == self node, 98 | /// in which case the system MAY decide to terminate as soon as possible, rather than stick around and tell others that it is leaving. 99 | /// Either scenarios are valid, with the "stick around to tell others we are down/leaving" being a "graceful leaving" scenario. 100 | /// 2. failure detector driven, unreachable: 101 | /// - SWIM detects node(s) as potentially dead, rather than marking them `.dead` immediately it marks them as `.unreachable` 102 | /// - it notifies clusterShell with `.unreachable(node)` 103 | /// - the shell updates its `membership` to reflect the reachability status of given `node`; if users subscribe to reachability events, 104 | /// such events are emitted from here 105 | /// - (TODO: this can just be an peer listening to events once we have events subbing) the shell queries `downingProvider` for decision for downing the node 106 | /// - the downing provider MAY invoke `cluster.down()` based on its logic and reachability information 107 | /// - iff `cluster.down(node)` is issued, the same steps as in 1. are taken, leading to the downing of the node in question 108 | /// 3. failure detector driven, dead: 109 | /// - SWIM detects `.dead` members in its failure detection gossip (as a result of 1. or 2.), immediately marking them `.dead` and invoking `cluster.down(node)` 110 | /// ~ (the following steps are exactly 1., however with pointing out one important decision in the SWIMShell) 111 | /// - `clusterShell` marks the node(s) as `.down`, and as it is the same code path as 1. and 2., also confirms to SWIM that `.confirmDead` 112 | /// - SWIM already knows those nodes are dead, and thus ignores the update, yet may continue to proceed gossiping the `.dead` information, 113 | /// e.g. until all nodes are informed of this fact 114 | case confirmDead(Node) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/SWIMNIOExample/NIOPeer.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020-2022 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import Logging 17 | import NIO 18 | import NIOConcurrencyHelpers 19 | import SWIM 20 | 21 | extension SWIM { 22 | /// SWIMPeer designed to deliver messages over UDP in collaboration with the SWIMNIOHandler. 23 | public actor NIOPeer: SWIMPeer, SWIMPingOriginPeer, SWIMPingRequestOriginPeer, CustomStringConvertible { 24 | public let swimNode: ClusterMembership.Node 25 | internal nonisolated var node: ClusterMembership.Node { 26 | self.swimNode 27 | } 28 | 29 | internal let channel: Channel 30 | 31 | public init(node: Node, channel: Channel) { 32 | self.swimNode = node 33 | self.channel = channel 34 | } 35 | 36 | public func ping( 37 | payload: GossipPayload, 38 | from origin: SWIM.NIOPeer, 39 | timeout: Swift.Duration, 40 | sequenceNumber: SWIM.SequenceNumber 41 | ) async throws -> PingResponse { 42 | try await withCheckedThrowingContinuation { continuation in 43 | let message = SWIM.Message.ping(replyTo: origin, payload: payload, sequenceNumber: sequenceNumber) 44 | let command = SWIMNIOWriteCommand( 45 | message: message, 46 | to: self.swimNode, 47 | replyTimeout: timeout.toNIO, 48 | replyCallback: { reply in 49 | switch reply { 50 | case .success(.response(.nack(_, _))): 51 | continuation.resume( 52 | throwing: SWIMNIOIllegalMessageTypeError( 53 | "Unexpected .nack reply to .ping message! Was: \(reply)" 54 | ) 55 | ) 56 | 57 | case .success(.response(let pingResponse)): 58 | assert( 59 | sequenceNumber == pingResponse.sequenceNumber, 60 | "callback invoked with not matching sequence number! Submitted with \(sequenceNumber) but invoked with \(pingResponse.sequenceNumber)!" 61 | ) 62 | continuation.resume(returning: pingResponse) 63 | 64 | case .failure(let error): 65 | continuation.resume(throwing: error) 66 | 67 | case .success(let other): 68 | continuation.resume( 69 | throwing: 70 | SWIMNIOIllegalMessageTypeError( 71 | "Unexpected message, got: [\(other)]:\(reflecting: type(of: other)) while expected \(PingResponse.self)" 72 | ) 73 | ) 74 | } 75 | } 76 | ) 77 | 78 | self.channel.writeAndFlush(command, promise: nil) 79 | } 80 | } 81 | 82 | public func pingRequest( 83 | target: SWIM.NIOPeer, 84 | payload: GossipPayload, 85 | from origin: SWIM.NIOPeer, 86 | timeout: Duration, 87 | sequenceNumber: SWIM.SequenceNumber 88 | ) async throws -> PingResponse { 89 | try await withCheckedThrowingContinuation { continuation in 90 | let message = SWIM.Message.pingRequest( 91 | target: target, 92 | replyTo: origin, 93 | payload: payload, 94 | sequenceNumber: sequenceNumber 95 | ) 96 | let command = SWIMNIOWriteCommand( 97 | message: message, 98 | to: self.node, 99 | replyTimeout: timeout.toNIO, 100 | replyCallback: { reply in 101 | switch reply { 102 | case .success(.response(let pingResponse)): 103 | assert( 104 | sequenceNumber == pingResponse.sequenceNumber, 105 | "callback invoked with not matching sequence number! Submitted with \(sequenceNumber) but invoked with \(pingResponse.sequenceNumber)!" 106 | ) 107 | continuation.resume(returning: pingResponse) 108 | 109 | case .failure(let error): 110 | continuation.resume(throwing: error) 111 | 112 | case .success(let other): 113 | continuation.resume( 114 | throwing: SWIMNIOIllegalMessageTypeError( 115 | "Unexpected message, got: \(other) while expected \(PingResponse.self)" 116 | ) 117 | ) 118 | } 119 | } 120 | ) 121 | 122 | self.channel.writeAndFlush(command, promise: nil) 123 | } 124 | } 125 | 126 | public func ack( 127 | acknowledging sequenceNumber: SWIM.SequenceNumber, 128 | target: SWIM.NIOPeer, 129 | incarnation: Incarnation, 130 | payload: GossipPayload 131 | ) { 132 | let message = SWIM.Message.response( 133 | .ack(target: target, incarnation: incarnation, payload: payload, sequenceNumber: sequenceNumber) 134 | ) 135 | let command = SWIMNIOWriteCommand( 136 | message: message, 137 | to: self.node, 138 | replyTimeout: .seconds(0), 139 | replyCallback: nil 140 | ) 141 | 142 | self.channel.writeAndFlush(command, promise: nil) 143 | } 144 | 145 | public func nack( 146 | acknowledging sequenceNumber: SWIM.SequenceNumber, 147 | target: SWIM.NIOPeer 148 | ) { 149 | let message = SWIM.Message.response(.nack(target: target, sequenceNumber: sequenceNumber)) 150 | let command = SWIMNIOWriteCommand( 151 | message: message, 152 | to: self.node, 153 | replyTimeout: .seconds(0), 154 | replyCallback: nil 155 | ) 156 | 157 | self.channel.writeAndFlush(command, promise: nil) 158 | } 159 | 160 | public nonisolated var description: String { 161 | "NIOPeer(\(self.node))" 162 | } 163 | } 164 | } 165 | 166 | extension SWIM.NIOPeer: Hashable { 167 | public nonisolated func hash(into hasher: inout Hasher) { 168 | self.node.hash(into: &hasher) 169 | } 170 | 171 | public static func == (lhs: SWIM.NIOPeer, rhs: SWIM.NIOPeer) -> Bool { 172 | lhs.node == rhs.node 173 | } 174 | } 175 | 176 | public struct SWIMNIOTimeoutError: Error, CustomStringConvertible { 177 | let timeout: Duration 178 | let message: String 179 | 180 | init(timeout: NIO.TimeAmount, message: String) { 181 | self.timeout = .nanoseconds(Int(timeout.nanoseconds)) 182 | self.message = message 183 | } 184 | 185 | init(timeout: Duration, message: String) { 186 | self.timeout = timeout 187 | self.message = message 188 | } 189 | 190 | public var description: String { 191 | "SWIMNIOTimeoutError(timeout: \(self.timeout.prettyDescription), \(self.message))" 192 | } 193 | } 194 | 195 | public struct SWIMNIOIllegalMessageTypeError: Error, CustomStringConvertible { 196 | let message: String 197 | 198 | init(_ message: String) { 199 | self.message = message 200 | } 201 | 202 | public var description: String { 203 | "SWIMNIOIllegalMessageTypeError(\(self.message))" 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Sources/SWIMNIOExample/Settings.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import Logging 17 | import NIO 18 | import SWIM 19 | 20 | /// Namespace for SWIMNIO constants. 21 | public enum SWIMNIO {} 22 | 23 | extension SWIMNIO { 24 | /// SWIMNIO specific settings. 25 | public struct Settings { 26 | /// Underlying settings for the SWIM protocol implementation. 27 | public var swim: SWIM.Settings 28 | 29 | public init() { 30 | self.init(swim: .init()) 31 | } 32 | 33 | public init(swim: SWIM.Settings) { 34 | self.swim = swim 35 | self.logger = swim.logger 36 | } 37 | 38 | /// The node as which this SWIMNIO shell should be bound. 39 | /// 40 | /// - SeeAlso: `SWIM.Settings.node` 41 | public var node: Node? { 42 | get { 43 | self.swim.node 44 | } 45 | set { 46 | self.swim.node = newValue 47 | } 48 | } 49 | 50 | // ==== Settings specific to SWIMNIO --------------------------------------------------------------------------- 51 | 52 | /// Allows for customizing the used logger. 53 | /// By default the same as passed in `swim.logger` in the initializer is used. 54 | public var logger: Logger 55 | 56 | // TODO: retry initial contact points max count: https://github.com/apple/swift-cluster-membership/issues/32 57 | 58 | /// How frequently the shell should retry attempting to join a `swim.initialContactPoint` 59 | public var initialContactPointPingInterval: TimeAmount = .seconds(5) 60 | 61 | /// For testing only, as it effectively disables the swim protocol period ticks. 62 | /// 63 | /// Allows for disabling of the periodically scheduled protocol period ticks. 64 | internal var _startPeriodicPingTimer: Bool = true 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/SWIMNIOExample/Utils/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | // ==== ---------------------------------------------------------------------------------------------------------------- 16 | // MARK: String Interpolation: reflecting: 17 | 18 | extension String.StringInterpolation { 19 | mutating func appendInterpolation(reflecting subject: Any?) { 20 | self.appendLiteral(String(reflecting: subject)) 21 | } 22 | 23 | mutating func appendInterpolation(reflecting subject: Any) { 24 | self.appendLiteral(String(reflecting: subject)) 25 | } 26 | } 27 | 28 | // ==== ---------------------------------------------------------------------------------------------------------------- 29 | // MARK: String Interpolation: lineByLine: 30 | 31 | extension String.StringInterpolation { 32 | mutating func appendInterpolation(lineByLine subject: [Any]) { 33 | self.appendLiteral("\n \(subject.map { "\($0)" }.joined(separator: "\n "))") 34 | } 35 | } 36 | 37 | // ==== ---------------------------------------------------------------------------------------------------------------- 38 | // MARK: String Interpolation: _:orElse: 39 | 40 | extension String.StringInterpolation { 41 | mutating func appendInterpolation(_ value: T?, orElse defaultValue: String) { 42 | self.appendLiteral("\(value.map { "\($0)" } ?? defaultValue)") 43 | } 44 | 45 | mutating func appendInterpolation(optional value: T?) { 46 | self.appendLiteral("\(value.map { "\($0)" } ?? "nil")") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/SWIMNIOExample/Utils/time.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIO 16 | import SWIM 17 | 18 | extension Swift.Duration { 19 | typealias Value = Int64 20 | 21 | var nanoseconds: Value { 22 | let (seconds, attoseconds) = self.components 23 | let sNanos = seconds * Value(1_000_000_000) 24 | let asNanos = attoseconds / Value(1_000_000_000) 25 | let (totalNanos, overflow) = sNanos.addingReportingOverflow(asNanos) 26 | return overflow ? .max : totalNanos 27 | } 28 | 29 | /// The microseconds representation of the `TimeAmount`. 30 | var microseconds: Value { 31 | self.nanoseconds / TimeUnit.microseconds.rawValue 32 | } 33 | 34 | /// The milliseconds representation of the `TimeAmount`. 35 | var milliseconds: Value { 36 | self.nanoseconds / TimeUnit.milliseconds.rawValue 37 | } 38 | 39 | /// The seconds representation of the `TimeAmount`. 40 | var seconds: Value { 41 | self.nanoseconds / TimeUnit.seconds.rawValue 42 | } 43 | 44 | var isEffectivelyInfinite: Bool { 45 | self.nanoseconds == .max 46 | } 47 | 48 | var toNIO: NIO.TimeAmount { 49 | .nanoseconds(self.nanoseconds) 50 | } 51 | 52 | /// Represents number of nanoseconds within given time unit 53 | enum TimeUnit: Value { 54 | case days = 86_400_000_000_000 55 | case hours = 3_600_000_000_000 56 | case minutes = 60_000_000_000 57 | case seconds = 1_000_000_000 58 | case milliseconds = 1_000_000 59 | case microseconds = 1000 60 | case nanoseconds = 1 61 | 62 | var abbreviated: String { 63 | switch self { 64 | case .nanoseconds: return "ns" 65 | case .microseconds: return "μs" 66 | case .milliseconds: return "ms" 67 | case .seconds: return "s" 68 | case .minutes: return "m" 69 | case .hours: return "h" 70 | case .days: return "d" 71 | } 72 | } 73 | 74 | func duration(_ duration: Int) -> Duration { 75 | switch self { 76 | case .nanoseconds: return .nanoseconds(Value(duration)) 77 | case .microseconds: return .microseconds(Value(duration)) 78 | case .milliseconds: return .milliseconds(Value(duration)) 79 | case .seconds: return .seconds(Value(duration)) 80 | case .minutes: return .seconds(Value(duration) * 60) 81 | case .hours: return .seconds(Value(duration) * 60 * 60) 82 | case .days: return .seconds(Value(duration) * 24 * 60 * 60) 83 | } 84 | } 85 | } 86 | } 87 | 88 | protocol PrettyTimeAmountDescription { 89 | var nanoseconds: Int64 { get } 90 | var isEffectivelyInfinite: Bool { get } 91 | 92 | var prettyDescription: String { get } 93 | func prettyDescription(precision: Int) -> String 94 | } 95 | 96 | extension PrettyTimeAmountDescription { 97 | var prettyDescription: String { 98 | self.prettyDescription() 99 | } 100 | 101 | func prettyDescription(precision: Int = 2) -> String { 102 | assert(precision > 0, "precision MUST BE > 0") 103 | if self.isEffectivelyInfinite { 104 | return "∞ (infinite)" 105 | } 106 | 107 | var res = "" 108 | var remainingNanos = self.nanoseconds 109 | 110 | if remainingNanos < 0 { 111 | res += "-" 112 | remainingNanos = remainingNanos * -1 113 | } 114 | 115 | var i = 0 116 | while i < precision { 117 | let unit = self.chooseUnit(remainingNanos) 118 | 119 | let rounded = Int(remainingNanos / unit.rawValue) 120 | if rounded > 0 { 121 | res += i > 0 ? " " : "" 122 | res += "\(rounded)\(unit.abbreviated)" 123 | 124 | remainingNanos = remainingNanos - unit.timeAmount(rounded).nanoseconds 125 | i += 1 126 | } else { 127 | break 128 | } 129 | } 130 | 131 | return res 132 | } 133 | 134 | private func chooseUnit(_ ns: Int64) -> PrettyTimeUnit { 135 | if ns / PrettyTimeUnit.days.rawValue > 0 { 136 | return PrettyTimeUnit.days 137 | } else if ns / PrettyTimeUnit.hours.rawValue > 0 { 138 | return PrettyTimeUnit.hours 139 | } else if ns / PrettyTimeUnit.minutes.rawValue > 0 { 140 | return PrettyTimeUnit.minutes 141 | } else if ns / PrettyTimeUnit.seconds.rawValue > 0 { 142 | return PrettyTimeUnit.seconds 143 | } else if ns / PrettyTimeUnit.milliseconds.rawValue > 0 { 144 | return PrettyTimeUnit.milliseconds 145 | } else if ns / PrettyTimeUnit.microseconds.rawValue > 0 { 146 | return PrettyTimeUnit.microseconds 147 | } else { 148 | return PrettyTimeUnit.nanoseconds 149 | } 150 | } 151 | } 152 | 153 | /// Represents number of nanoseconds within given time unit 154 | enum PrettyTimeUnit: Int64 { 155 | case days = 86_400_000_000_000 156 | case hours = 3_600_000_000_000 157 | case minutes = 60_000_000_000 158 | case seconds = 1_000_000_000 159 | case milliseconds = 1_000_000 160 | case microseconds = 1000 161 | case nanoseconds = 1 162 | 163 | var abbreviated: String { 164 | switch self { 165 | case .nanoseconds: return "ns" 166 | case .microseconds: return "μs" 167 | case .milliseconds: return "ms" 168 | case .seconds: return "s" 169 | case .minutes: return "m" 170 | case .hours: return "h" 171 | case .days: return "d" 172 | } 173 | } 174 | 175 | func timeAmount(_ amount: Int) -> TimeAmount { 176 | switch self { 177 | case .nanoseconds: return .nanoseconds(Int64(amount)) 178 | case .microseconds: return .microseconds(Int64(amount)) 179 | case .milliseconds: return .milliseconds(Int64(amount)) 180 | case .seconds: return .seconds(Int64(amount)) 181 | case .minutes: return .minutes(Int64(amount)) 182 | case .hours: return .hours(Int64(amount)) 183 | case .days: return .hours(Int64(amount) * 24) 184 | } 185 | } 186 | } 187 | 188 | extension NIO.TimeAmount: PrettyTimeAmountDescription { 189 | var isEffectivelyInfinite: Bool { 190 | self.nanoseconds == .max 191 | } 192 | } 193 | 194 | extension Swift.Duration: PrettyTimeAmountDescription {} 195 | -------------------------------------------------------------------------------- /Tests/ClusterMembershipDocumentationTests/SWIMDocExamples.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | // tag::imports[] 16 | 17 | import SWIM 18 | import XCTest 19 | 20 | // end::imports[] 21 | 22 | final class SWIMDocExamples: XCTestCase {} 23 | -------------------------------------------------------------------------------- /Tests/ClusterMembershipTests/NodeTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import XCTest 16 | 17 | @testable import ClusterMembership 18 | 19 | final class NodeTests: XCTestCase { 20 | let firstNode = ClusterMembership.Node(protocol: "test", host: "127.0.0.1", port: 7001, uid: 1111) 21 | let secondNode = ClusterMembership.Node(protocol: "test", host: "127.0.0.1", port: 7002, uid: 2222) 22 | let thirdNode = ClusterMembership.Node(protocol: "test", host: "127.0.0.2", port: 7001, uid: 3333) 23 | 24 | func testCompareSameProtocolAndHost() throws { 25 | XCTAssertLessThan(self.firstNode, self.secondNode) 26 | XCTAssertGreaterThan(self.secondNode, self.firstNode) 27 | XCTAssertNotEqual(self.firstNode, self.secondNode) 28 | } 29 | 30 | func testCompareDifferentHost() throws { 31 | XCTAssertLessThan(self.firstNode, self.thirdNode) 32 | XCTAssertGreaterThan(self.thirdNode, self.firstNode) 33 | XCTAssertNotEqual(self.firstNode, self.thirdNode) 34 | XCTAssertLessThan(self.secondNode, self.thirdNode) 35 | XCTAssertGreaterThan(self.thirdNode, self.secondNode) 36 | } 37 | 38 | func testSort() throws { 39 | let nodes: Set = [secondNode, firstNode, thirdNode] 40 | let sorted_nodes = nodes.sorted() 41 | 42 | XCTAssertEqual(sorted_nodes, [self.firstNode, self.secondNode, self.thirdNode]) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/SWIMNIOExampleTests/CodingTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import Foundation 17 | import NIO 18 | import SWIM 19 | import XCTest 20 | 21 | @testable import SWIMNIOExample 22 | 23 | final class CodingTests: XCTestCase { 24 | lazy var nioPeer: SWIM.NIOPeer = SWIM.NIOPeer( 25 | node: .init(protocol: "udp", host: "127.0.0.1", port: 1111, uid: 12121), 26 | channel: EmbeddedChannel() 27 | ) 28 | lazy var nioPeerOther: SWIM.NIOPeer = SWIM.NIOPeer( 29 | node: .init(protocol: "udp", host: "127.0.0.1", port: 2222, uid: 234_324), 30 | channel: EmbeddedChannel() 31 | ) 32 | 33 | lazy var memberOne = SWIM.Member(peer: nioPeer, status: .alive(incarnation: 1), protocolPeriod: 0) 34 | lazy var memberTwo = SWIM.Member(peer: nioPeer, status: .alive(incarnation: 2), protocolPeriod: 0) 35 | lazy var memberThree = SWIM.Member(peer: nioPeer, status: .alive(incarnation: 2), protocolPeriod: 0) 36 | 37 | // TODO: add some more "nasty" cases, since the node parsing code is very manual and not hardened / secure 38 | func test_serializationOf_node() throws { 39 | try self.shared_serializationRoundtrip( 40 | ContainsNode(node: Node(protocol: "udp", host: "127.0.0.1", port: 1111, uid: 12121)) 41 | ) 42 | try self.shared_serializationRoundtrip( 43 | ContainsNode(node: Node(protocol: "udp", host: "127.0.0.1", port: 1111, uid: nil)) 44 | ) 45 | try self.shared_serializationRoundtrip( 46 | ContainsNode(node: Node(protocol: "udp", host: "127.0.0.1", port: 1111, uid: .random(in: 0...UInt64.max))) 47 | ) 48 | try self.shared_serializationRoundtrip( 49 | Node(protocol: "udp", host: "127.0.0.1", port: 1111, uid: .random(in: 0...UInt64.max)) 50 | ) 51 | 52 | // with name 53 | try self.shared_serializationRoundtrip( 54 | Node(protocol: "udp", name: "kappa", host: "127.0.0.1", port: 2222, uid: .random(in: 0...UInt64.max)) 55 | ) 56 | } 57 | 58 | func test_serializationOf_peer() throws { 59 | try self.shared_serializationRoundtrip(ContainsPeer(peer: self.nioPeer)) 60 | } 61 | 62 | func test_serializationOf_member() throws { 63 | try self.shared_serializationRoundtrip(ContainsMember(member: self.memberOne)) 64 | } 65 | 66 | func test_serializationOf_ping() throws { 67 | let payloadSome: SWIM.GossipPayload = .membership([ 68 | self.memberOne, 69 | self.memberTwo, 70 | self.memberThree, 71 | ]) 72 | try self.shared_serializationRoundtrip( 73 | SWIM.Message.ping(replyTo: self.nioPeer, payload: payloadSome, sequenceNumber: 1212) 74 | ) 75 | } 76 | 77 | func test_serializationOf_pingReq() throws { 78 | let payloadNone: SWIM.GossipPayload = .none 79 | try self.shared_serializationRoundtrip( 80 | SWIM.Message.pingRequest( 81 | target: self.nioPeer, 82 | replyTo: self.nioPeerOther, 83 | payload: payloadNone, 84 | sequenceNumber: 111 85 | ) 86 | ) 87 | 88 | let payloadSome: SWIM.GossipPayload = .membership([ 89 | self.memberOne, 90 | self.memberTwo, 91 | self.memberThree, 92 | ]) 93 | try self.shared_serializationRoundtrip( 94 | SWIM.Message.pingRequest( 95 | target: self.nioPeer, 96 | replyTo: self.nioPeerOther, 97 | payload: payloadSome, 98 | sequenceNumber: 1212 99 | ) 100 | ) 101 | } 102 | 103 | // ==== ------------------------------------------------------------------------------------------------------------ 104 | // MARK: Utils 105 | 106 | func shared_serializationRoundtrip(_ obj: T) throws { 107 | let repr = try SWIMNIODefaultEncoder().encode(obj) 108 | let decoder = SWIMNIODefaultDecoder() 109 | decoder.userInfo[.channelUserInfoKey] = EmbeddedChannel() 110 | let deserialized = try decoder.decode(T.self, from: repr) 111 | 112 | XCTAssertEqual("\(obj)", "\(deserialized)") 113 | } 114 | } 115 | 116 | // This is a workaround until Swift 5.2.5 is available with the "top level string value encoding" support. 117 | struct ContainsPeer: Codable { 118 | let peer: SWIM.NIOPeer 119 | } 120 | 121 | // This is a workaround until Swift 5.2.5 is available with the "top level string value encoding" support. 122 | struct ContainsMember: Codable { 123 | let member: SWIM.Member 124 | } 125 | 126 | // This is a workaround until Swift 5.2.5 is available with the "top level string value encoding" support. 127 | struct ContainsNode: Codable { 128 | let node: ClusterMembership.Node 129 | } 130 | -------------------------------------------------------------------------------- /Tests/SWIMNIOExampleTests/SWIMNIOClusteredTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import Logging 17 | import NIO 18 | import SWIM 19 | import SWIMTestKit 20 | import XCTest 21 | 22 | @testable import SWIMNIOExample 23 | 24 | final class SWIMNIOClusteredTests: RealClusteredXCTestCase { 25 | // ==== ------------------------------------------------------------------------------------------------------------ 26 | // MARK: White box tests // TODO: implement more of the tests in terms of inspecting events 27 | 28 | // ==== ------------------------------------------------------------------------------------------------------------ 29 | // MARK: Black box tests, we let the nodes run and inspect their state via logs 30 | 31 | func test_real_peers_2_connect() throws { 32 | let (firstHandler, _) = self.makeClusterNode() 33 | 34 | let (secondHandler, _) = self.makeClusterNode { settings in 35 | settings.swim.initialContactPoints = [firstHandler.shell.node] 36 | } 37 | 38 | try self.capturedLogs(of: firstHandler.shell.node) 39 | .awaitLog(grep: #""swim/members/count": 2"#) 40 | try self.capturedLogs(of: secondHandler.shell.node) 41 | .awaitLog(grep: #""swim/members/count": 2"#) 42 | } 43 | 44 | func test_real_peers_2_connect_first_terminates() throws { 45 | let (firstHandler, firstChannel) = self.makeClusterNode { settings in 46 | settings.swim.pingTimeout = .milliseconds(100) 47 | settings.swim.probeInterval = .milliseconds(500) 48 | } 49 | 50 | let (secondHandler, _) = self.makeClusterNode { settings in 51 | settings.swim.initialContactPoints = [firstHandler.shell.node] 52 | 53 | settings.swim.pingTimeout = .milliseconds(100) 54 | settings.swim.probeInterval = .milliseconds(500) 55 | } 56 | 57 | try self.capturedLogs(of: firstHandler.shell.node) 58 | .awaitLog(grep: #""swim/members/count": 2"#) 59 | 60 | // close first channel 61 | firstHandler.log.warning("Stopping \(firstHandler.shell.node)...") 62 | secondHandler.log.warning("Stopping \(firstHandler.shell.node)...") 63 | try firstChannel.close().wait() 64 | 65 | // we should get back down to a 1 node cluster 66 | // TODO: add same tests but embedded 67 | try self.capturedLogs(of: secondHandler.shell.node) 68 | .awaitLog(grep: #""swim/suspects/count": 1"#, within: .seconds(20)) 69 | } 70 | 71 | func test_real_peers_2_connect_peerCountNeverExceeds2() throws { 72 | let (firstHandler, _) = self.makeClusterNode { settings in 73 | settings.swim.pingTimeout = .milliseconds(100) 74 | settings.swim.probeInterval = .milliseconds(500) 75 | } 76 | 77 | let (secondHandler, _) = self.makeClusterNode { settings in 78 | settings.swim.initialContactPoints = [firstHandler.shell.node] 79 | 80 | settings.swim.pingTimeout = .milliseconds(100) 81 | settings.swim.probeInterval = .milliseconds(500) 82 | } 83 | 84 | try self.capturedLogs(of: firstHandler.shell.node) 85 | .awaitLog(grep: #""swim/members/count": 2"#) 86 | 87 | sleep(5) 88 | 89 | do { 90 | let found = try self.capturedLogs(of: secondHandler.shell.node) 91 | .awaitLog(grep: #""swim/members/count": 3"#, within: .seconds(5)) 92 | XCTFail("Found unexpected members count: 3! Log message: \(found)") 93 | return 94 | } catch { 95 | () // good! 96 | } 97 | } 98 | 99 | func test_real_peers_5_connect() throws { 100 | let (first, _) = self.makeClusterNode { settings in 101 | settings.swim.probeInterval = .milliseconds(200) 102 | } 103 | let (second, _) = self.makeClusterNode { settings in 104 | settings.swim.probeInterval = .milliseconds(200) 105 | settings.swim.initialContactPoints = [first.shell.node] 106 | } 107 | let (third, _) = self.makeClusterNode { settings in 108 | settings.swim.probeInterval = .milliseconds(200) 109 | settings.swim.initialContactPoints = [second.shell.node] 110 | } 111 | let (fourth, _) = self.makeClusterNode { settings in 112 | settings.swim.probeInterval = .milliseconds(200) 113 | settings.swim.initialContactPoints = [third.shell.node] 114 | } 115 | let (fifth, _) = self.makeClusterNode { settings in 116 | settings.swim.probeInterval = .milliseconds(200) 117 | settings.swim.initialContactPoints = [fourth.shell.node] 118 | } 119 | 120 | try [first, second, third, fourth, fifth].forEach { handler in 121 | do { 122 | try self.capturedLogs(of: handler.shell.node) 123 | .awaitLog( 124 | grep: #""swim/members/count": 5"#, 125 | within: .seconds(5) 126 | ) 127 | } catch { 128 | throw TestError("Failed to find expected logs on \(handler.shell.node)", error: error) 129 | } 130 | } 131 | } 132 | 133 | func test_real_peers_5_connect_butSlowly() throws { 134 | let (first, _) = self.makeClusterNode { settings in 135 | settings.swim.pingTimeout = .milliseconds(100) 136 | settings.swim.probeInterval = .milliseconds(500) 137 | } 138 | let (second, _) = self.makeClusterNode { settings in 139 | settings.swim.initialContactPoints = [first.shell.node] 140 | settings.swim.pingTimeout = .milliseconds(100) 141 | settings.swim.probeInterval = .milliseconds(500) 142 | } 143 | // we sleep in order to ensure we exhaust the "gossip at most ... times" logic 144 | sleep(4) 145 | let (third, _) = self.makeClusterNode { settings in 146 | settings.swim.initialContactPoints = [second.shell.node] 147 | settings.swim.pingTimeout = .milliseconds(100) 148 | settings.swim.probeInterval = .milliseconds(500) 149 | } 150 | let (fourth, _) = self.makeClusterNode { settings in 151 | settings.swim.initialContactPoints = [third.shell.node] 152 | settings.swim.pingTimeout = .milliseconds(100) 153 | settings.swim.probeInterval = .milliseconds(500) 154 | } 155 | // after joining two more, we sleep again to make sure they all exhaust their gossip message counts 156 | sleep(2) 157 | let (fifth, _) = self.makeClusterNode { settings in 158 | // we connect fir the first, they should exchange all information 159 | settings.swim.initialContactPoints = [ 160 | first.shell.node, 161 | fourth.shell.node, 162 | ] 163 | } 164 | 165 | try [first, second, third, fourth, fifth].forEach { handler in 166 | do { 167 | try self.capturedLogs(of: handler.shell.node) 168 | .awaitLog( 169 | grep: #""swim/members/count": 5"#, 170 | within: .seconds(5) 171 | ) 172 | } catch { 173 | throw TestError("Failed to find expected logs on \(handler.shell.node)", error: error) 174 | } 175 | } 176 | } 177 | 178 | func test_real_peers_5_then1Dies_becomesSuspect() throws { 179 | let (first, firstChannel) = self.makeClusterNode { settings in 180 | settings.swim.pingTimeout = .milliseconds(100) 181 | settings.swim.probeInterval = .milliseconds(500) 182 | } 183 | let (second, _) = self.makeClusterNode { settings in 184 | settings.swim.initialContactPoints = [first.shell.node] 185 | settings.swim.pingTimeout = .milliseconds(100) 186 | settings.swim.probeInterval = .milliseconds(500) 187 | } 188 | let (third, _) = self.makeClusterNode { settings in 189 | settings.swim.initialContactPoints = [second.shell.node] 190 | settings.swim.pingTimeout = .milliseconds(100) 191 | settings.swim.probeInterval = .milliseconds(500) 192 | } 193 | let (fourth, _) = self.makeClusterNode { settings in 194 | settings.swim.initialContactPoints = [third.shell.node] 195 | settings.swim.pingTimeout = .milliseconds(100) 196 | settings.swim.probeInterval = .milliseconds(500) 197 | } 198 | let (fifth, _) = self.makeClusterNode { settings in 199 | settings.swim.initialContactPoints = [fourth.shell.node] 200 | settings.swim.pingTimeout = .milliseconds(100) 201 | settings.swim.probeInterval = .milliseconds(500) 202 | } 203 | 204 | try [first, second, third, fourth, fifth].forEach { handler in 205 | do { 206 | try self.capturedLogs(of: handler.shell.node) 207 | .awaitLog( 208 | grep: #""swim/members/count": 5"#, 209 | within: .seconds(20) 210 | ) 211 | } catch { 212 | throw TestError("Failed to find expected logs on \(handler.shell.node)", error: error) 213 | } 214 | } 215 | 216 | try firstChannel.close().wait() 217 | 218 | try [second, third, fourth, fifth].forEach { handler in 219 | do { 220 | try self.capturedLogs(of: handler.shell.node) 221 | .awaitLog( 222 | grep: #""swim/suspects/count": 1"#, 223 | within: .seconds(10) 224 | ) 225 | } catch { 226 | throw TestError("Failed to find expected logs on \(handler.shell.node)", error: error) 227 | } 228 | } 229 | } 230 | 231 | // ==== ---------------------------------------------------------------------------------------------------------------- 232 | // MARK: nack tests 233 | 234 | func test_real_pingRequestsGetSent_nacksArriveBack() throws { 235 | let (firstHandler, _) = self.makeClusterNode() 236 | let (secondHandler, _) = self.makeClusterNode { settings in 237 | settings.swim.initialContactPoints = [firstHandler.shell.node] 238 | } 239 | let (thirdHandler, thirdChannel) = self.makeClusterNode { settings in 240 | settings.swim.initialContactPoints = [firstHandler.shell.node, secondHandler.shell.node] 241 | } 242 | 243 | try self.capturedLogs(of: firstHandler.shell.node) 244 | .awaitLog(grep: #""swim/members/count": 3"#) 245 | try self.capturedLogs(of: secondHandler.shell.node) 246 | .awaitLog(grep: #""swim/members/count": 3"#) 247 | try self.capturedLogs(of: thirdHandler.shell.node) 248 | .awaitLog(grep: #""swim/members/count": 3"#) 249 | 250 | try thirdChannel.close().wait() 251 | 252 | try self.capturedLogs(of: firstHandler.shell.node) 253 | .awaitLog(grep: "Read successful: response/nack") 254 | try self.capturedLogs(of: secondHandler.shell.node) 255 | .awaitLog(grep: "Read successful: response/nack") 256 | 257 | try self.capturedLogs(of: firstHandler.shell.node) 258 | .awaitLog(grep: #""swim/suspects/count": 1"#) 259 | try self.capturedLogs(of: secondHandler.shell.node) 260 | .awaitLog(grep: #""swim/suspects/count": 1"#) 261 | } 262 | } 263 | 264 | private struct TestError: Error { 265 | let message: String 266 | let error: Error 267 | 268 | init(_ message: String, error: Error) { 269 | self.message = message 270 | self.error = error 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /Tests/SWIMNIOExampleTests/SWIMNIOEventClusteredTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import NIO 17 | import SWIM 18 | import SWIMTestKit 19 | import XCTest 20 | 21 | @testable import SWIMNIOExample 22 | 23 | // TODO: those tests could be done on embedded event loops probably 24 | final class SWIMNIOEventClusteredTests: EmbeddedClusteredXCTestCase { 25 | var settings: SWIMNIO.Settings = SWIMNIO.Settings(swim: .init()) 26 | lazy var myselfNode = Node(protocol: "udp", host: "127.0.0.1", port: 7001, uid: 1111) 27 | lazy var myselfPeer = SWIM.NIOPeer(node: myselfNode, channel: EmbeddedChannel()) 28 | lazy var myselfMemberAliveInitial = SWIM.Member(peer: myselfPeer, status: .alive(incarnation: 0), protocolPeriod: 0) 29 | 30 | var group: MultiThreadedEventLoopGroup! 31 | 32 | override func setUp() { 33 | super.setUp() 34 | 35 | self.settings.node = self.myselfNode 36 | 37 | self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 38 | } 39 | 40 | override func tearDown() { 41 | try! self.group.syncShutdownGracefully() 42 | self.group = nil 43 | super.tearDown() 44 | } 45 | 46 | func test_memberStatusChange_alive_emittedForMyself() throws { 47 | let firstProbe = ProbeEventHandler(loop: group.next()) 48 | 49 | let first = try bindShell(probe: firstProbe) { settings in 50 | settings.node = self.myselfNode 51 | } 52 | defer { try! first.close().wait() } 53 | 54 | try firstProbe.expectEvent( 55 | SWIM.MemberStatusChangedEvent(previousStatus: nil, member: self.myselfMemberAliveInitial) 56 | ) 57 | } 58 | 59 | func test_memberStatusChange_suspect_emittedForDyingNode() throws { 60 | let firstProbe = ProbeEventHandler(loop: group.next()) 61 | let secondProbe = ProbeEventHandler(loop: group.next()) 62 | 63 | let secondNodePort = 7002 64 | let secondNode = Node(protocol: "udp", host: "127.0.0.1", port: secondNodePort, uid: 222_222) 65 | 66 | let second = try bindShell(probe: secondProbe) { settings in 67 | settings.node = secondNode 68 | } 69 | 70 | let first = try bindShell(probe: firstProbe) { settings in 71 | settings.node = self.myselfNode 72 | settings.swim.initialContactPoints = [secondNode.withoutUID] 73 | } 74 | defer { try! first.close().wait() } 75 | 76 | // wait for second probe to become alive: 77 | try secondProbe.expectEvent( 78 | SWIM.MemberStatusChangedEvent( 79 | previousStatus: nil, 80 | member: SWIM.Member( 81 | peer: SWIM.NIOPeer(node: secondNode, channel: EmbeddedChannel()), 82 | status: .alive(incarnation: 0), 83 | protocolPeriod: 0 84 | ) 85 | ) 86 | ) 87 | 88 | sleep(5) // let them discover each other, since the nodes are slow at retrying and we didn't configure it yet a sleep is here meh 89 | try! second.close().wait() 90 | 91 | try firstProbe.expectEvent( 92 | SWIM.MemberStatusChangedEvent(previousStatus: nil, member: self.myselfMemberAliveInitial) 93 | ) 94 | 95 | let secondAliveEvent = try firstProbe.expectEvent() 96 | XCTAssertTrue(secondAliveEvent.isReachabilityChange) 97 | XCTAssertTrue(secondAliveEvent.status.isAlive) 98 | XCTAssertEqual(secondAliveEvent.member.node.withoutUID, secondNode.withoutUID) 99 | 100 | let secondDeadEvent = try firstProbe.expectEvent() 101 | XCTAssertTrue(secondDeadEvent.isReachabilityChange) 102 | XCTAssertTrue(secondDeadEvent.status.isDead) 103 | XCTAssertEqual(secondDeadEvent.member.node.withoutUID, secondNode.withoutUID) 104 | } 105 | 106 | private func bindShell( 107 | probe probeHandler: ProbeEventHandler, 108 | configure: (inout SWIMNIO.Settings) -> Void = { _ in () } 109 | ) throws -> Channel { 110 | var settings = self.settings 111 | configure(&settings) 112 | self.makeLogCapture(name: "swim-\(settings.node!.port)", settings: &settings) 113 | 114 | self._nodes.append(settings.node!) 115 | return try DatagramBootstrap(group: self.group) 116 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 117 | .channelInitializer { channel in 118 | 119 | let swimHandler = SWIMNIOHandler(settings: settings) 120 | return channel.pipeline.addHandler(swimHandler).flatMap { _ in 121 | channel.pipeline.addHandler(probeHandler) 122 | } 123 | }.bind(host: settings.node!.host, port: settings.node!.port).wait() 124 | } 125 | } 126 | 127 | // ==== ---------------------------------------------------------------------------------------------------------------- 128 | // MARK: Test Utils 129 | 130 | extension ProbeEventHandler { 131 | @discardableResult 132 | func expectEvent( 133 | _ expected: SWIM.MemberStatusChangedEvent? = nil, 134 | file: StaticString = (#file), 135 | line: UInt = #line 136 | ) throws -> SWIM.MemberStatusChangedEvent { 137 | let got = try self.expectEvent() 138 | 139 | if let expected = expected { 140 | XCTAssertEqual(got, expected, file: file, line: line) 141 | } 142 | 143 | return got 144 | } 145 | } 146 | 147 | final class ProbeEventHandler: ChannelInboundHandler { 148 | typealias InboundIn = SWIM.MemberStatusChangedEvent 149 | 150 | var events: [SWIM.MemberStatusChangedEvent] = [] 151 | var waitingPromise: EventLoopPromise>? 152 | var loop: EventLoop 153 | 154 | init(loop: EventLoop) { 155 | self.loop = loop 156 | } 157 | 158 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 159 | let change = self.unwrapInboundIn(data) 160 | self.events.append(change) 161 | 162 | if let probePromise = self.waitingPromise { 163 | let event = self.events.removeFirst() 164 | probePromise.succeed(event) 165 | self.waitingPromise = nil 166 | } 167 | } 168 | 169 | func expectEvent( 170 | file: StaticString = #file, 171 | line: UInt = #line 172 | ) throws -> SWIM.MemberStatusChangedEvent { 173 | let p = self.loop.makePromise(of: SWIM.MemberStatusChangedEvent.self, file: file, line: line) 174 | self.loop.execute { 175 | assert(self.waitingPromise == nil, "Already waiting on an event") 176 | if !self.events.isEmpty { 177 | let event = self.events.removeFirst() 178 | p.succeed(event) 179 | } else { 180 | self.waitingPromise = p 181 | } 182 | } 183 | return try p.futureResult.wait() 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Tests/SWIMNIOExampleTests/SWIMNIOMetricsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import Dispatch 17 | import Metrics 18 | import NIO 19 | import SWIMTestKit 20 | import XCTest 21 | 22 | @testable import CoreMetrics 23 | @testable import SWIM 24 | @testable import SWIMNIOExample 25 | 26 | final class SWIMNIOMetricsTests: RealClusteredXCTestCase { 27 | var testMetrics: TestMetrics! 28 | 29 | override func setUp() { 30 | super.setUp() 31 | 32 | self.testMetrics = TestMetrics() 33 | MetricsSystem.bootstrapInternal(self.testMetrics) 34 | } 35 | 36 | override func tearDown() { 37 | super.tearDown() 38 | MetricsSystem.bootstrapInternal(NOOPMetricsHandler.instance) 39 | } 40 | 41 | // ==== ------------------------------------------------------------------------------------------------------------ 42 | // MARK: Metrics tests 43 | 44 | func test_metrics_emittedByNIOImplementation() throws { 45 | let (firstHandler, _) = self.makeClusterNode { settings in 46 | settings.swim.metrics.labelPrefix = "first" 47 | settings.swim.probeInterval = .milliseconds(100) 48 | } 49 | _ = self.makeClusterNode { settings in 50 | settings.swim.metrics.labelPrefix = "second" 51 | settings.swim.probeInterval = .milliseconds(100) 52 | settings.swim.initialContactPoints = [firstHandler.shell.node] 53 | } 54 | let (_, thirdChannel) = self.makeClusterNode { settings in 55 | settings.swim.metrics.labelPrefix = "third" 56 | settings.swim.probeInterval = .milliseconds(100) 57 | settings.swim.initialContactPoints = [firstHandler.shell.node] 58 | } 59 | 60 | sleep(1) // giving it some extra time to report a few metrics (a few round-trip times etc). 61 | 62 | let m: SWIM.Metrics.ShellMetrics = firstHandler.metrics! 63 | 64 | let roundTripTime = try! self.testMetrics.expectTimer(m.pingResponseTime) 65 | XCTAssertNotNil(roundTripTime.lastValue) // some roundtrip time should have been reported 66 | for rtt in roundTripTime.values { 67 | print(" ping rtt recorded: \(TimeAmount.nanoseconds(rtt).prettyDescription)") 68 | } 69 | 70 | let messageInboundCount = try! self.testMetrics.expectCounter(m.messageInboundCount) 71 | let messageInboundBytes = try! self.testMetrics.expectRecorder(m.messageInboundBytes) 72 | print(" messageInboundCount = \(messageInboundCount.totalValue)") 73 | print(" messageInboundBytes = \(messageInboundBytes.lastValue!)") 74 | XCTAssertGreaterThan(messageInboundCount.totalValue, 0) 75 | XCTAssertGreaterThan(messageInboundBytes.lastValue!, 0) 76 | 77 | let messageOutboundCount = try! self.testMetrics.expectCounter(m.messageOutboundCount) 78 | let messageOutboundBytes = try! self.testMetrics.expectRecorder(m.messageOutboundBytes) 79 | print(" messageOutboundCount = \(messageOutboundCount.totalValue)") 80 | print(" messageOutboundBytes = \(messageOutboundBytes.lastValue!)") 81 | XCTAssertGreaterThan(messageOutboundCount.totalValue, 0) 82 | XCTAssertGreaterThan(messageOutboundBytes.lastValue!, 0) 83 | 84 | thirdChannel.close(promise: nil) 85 | sleep(2) 86 | 87 | let pingRequestResponseTimeAll = try! self.testMetrics.expectTimer(m.pingRequestResponseTimeAll) 88 | print(" pingRequestResponseTimeAll = \(pingRequestResponseTimeAll.lastValue!)") 89 | XCTAssertGreaterThan(pingRequestResponseTimeAll.lastValue!, 0) 90 | 91 | let pingRequestResponseTimeFirst = try! self.testMetrics.expectTimer(m.pingRequestResponseTimeFirst) 92 | XCTAssertNil(pingRequestResponseTimeFirst.lastValue) // because this only counts ACKs, and we get NACKs because the peer is down 93 | 94 | let successfulPingProbes = try! self.testMetrics.expectCounter( 95 | firstHandler.shell.swim.metrics.successfulPingProbes 96 | ) 97 | print(" successfulPingProbes = \(successfulPingProbes.totalValue)") 98 | XCTAssertGreaterThan(successfulPingProbes.totalValue, 1) // definitely at least one, we joined some nodes 99 | 100 | let failedPingProbes = try! self.testMetrics.expectCounter(firstHandler.shell.swim.metrics.failedPingProbes) 101 | print(" failedPingProbes = \(failedPingProbes.totalValue)") 102 | XCTAssertGreaterThan(failedPingProbes.totalValue, 1) // definitely at least one, we detected the down peer 103 | 104 | let successfulPingRequestProbes = try! self.testMetrics.expectCounter( 105 | firstHandler.shell.swim.metrics.successfulPingRequestProbes 106 | ) 107 | print(" successfulPingRequestProbes = \(successfulPingRequestProbes.totalValue)") 108 | XCTAssertGreaterThan(successfulPingRequestProbes.totalValue, 1) // definitely at least one, the second peer is alive and .nacks us, so we count that as success 109 | 110 | let failedPingRequestProbes = try! self.testMetrics.expectCounter( 111 | firstHandler.shell.swim.metrics.failedPingRequestProbes 112 | ) 113 | print(" failedPingRequestProbes = \(failedPingRequestProbes.totalValue)") 114 | XCTAssertEqual(failedPingRequestProbes.totalValue, 0) // 0 because the second peer is still responsive to us, even it third is dead 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tests/SWIMNIOExampleTests/Utils/BaseXCTestCases.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2018-2022 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import Logging 17 | import NIO 18 | import NIOCore 19 | import SWIM 20 | import SWIMTestKit 21 | import XCTest 22 | 23 | import struct Foundation.Date 24 | import class Foundation.NSLock 25 | 26 | @testable import SWIMNIOExample 27 | 28 | // ==== ---------------------------------------------------------------------------------------------------------------- 29 | // MARK: Real Networking Test Case 30 | 31 | class RealClusteredXCTestCase: BaseClusteredXCTestCase { 32 | var group: MultiThreadedEventLoopGroup! 33 | var loop: EventLoop! 34 | 35 | override func setUp() { 36 | super.setUp() 37 | 38 | self.group = MultiThreadedEventLoopGroup(numberOfThreads: 8) 39 | self.loop = group.next() 40 | } 41 | 42 | override func tearDown() { 43 | super.tearDown() 44 | 45 | try! self.group.syncShutdownGracefully() 46 | self.group = nil 47 | self.loop = nil 48 | } 49 | 50 | func makeClusterNode( 51 | name: String? = nil, 52 | configure configureSettings: (inout SWIMNIO.Settings) -> Void = { _ in () } 53 | ) -> (SWIMNIOHandler, Channel) { 54 | let port = self.nextPort() 55 | let name = name ?? "swim-\(port)" 56 | var settings = SWIMNIO.Settings() 57 | configureSettings(&settings) 58 | 59 | if self.captureLogs { 60 | self.makeLogCapture(name: name, settings: &settings) 61 | } 62 | 63 | let handler = SWIMNIOHandler(settings: settings) 64 | let bootstrap = DatagramBootstrap(group: self.group) 65 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 66 | .channelInitializer { channel in channel.pipeline.addHandler(handler) } 67 | 68 | let channel = try! bootstrap.bind(host: "127.0.0.1", port: port).wait() 69 | 70 | self._shells.append(handler.shell) 71 | self._nodes.append(handler.shell.node) 72 | 73 | return (handler, channel) 74 | } 75 | } 76 | 77 | // ==== ---------------------------------------------------------------------------------------------------------------- 78 | // MARK: Embedded Networking Test Case 79 | 80 | class EmbeddedClusteredXCTestCase: BaseClusteredXCTestCase { 81 | var loop: EmbeddedEventLoop! 82 | 83 | open override func setUp() { 84 | super.setUp() 85 | 86 | self.loop = EmbeddedEventLoop() 87 | } 88 | 89 | open override func tearDown() { 90 | super.tearDown() 91 | 92 | try! self.loop.close() 93 | self.loop = nil 94 | } 95 | 96 | func makeEmbeddedShell( 97 | _ _name: String? = nil, 98 | configure: (inout SWIMNIO.Settings) -> Void = { _ in () } 99 | ) -> SWIMNIOShell { 100 | var settings = SWIMNIO.Settings() 101 | configure(&settings) 102 | let node: Node 103 | if let _node = settings.swim.node { 104 | node = _node 105 | } else { 106 | let port = self.nextPort() 107 | let name = _name ?? "swim-\(port)" 108 | node = Node(protocol: "test", name: name, host: "127.0.0.1", port: port, uid: .random(in: 1.. Int { 155 | defer { self._nextPort += 1 } 156 | return self._nextPort 157 | } 158 | 159 | open func configureLogCapture(settings: inout LogCapture.Settings) { 160 | // just use defaults 161 | } 162 | 163 | open override func setUp() { 164 | super.setUp() 165 | 166 | self.addTeardownBlock { 167 | for shell in self._shells { 168 | do { 169 | try await shell.myself.channel.close() 170 | } catch { 171 | () // channel was already closed, that's okey (e.g. we closed it in the test to "crash" a node) 172 | } 173 | } 174 | } 175 | } 176 | 177 | open override func tearDown() { 178 | super.tearDown() 179 | 180 | let testsFailed = self.testRun?.totalFailureCount ?? 0 > 0 181 | if self.captureLogs, self.alwaysPrintCaptureLogs || testsFailed { 182 | self.printAllCapturedLogs() 183 | } 184 | 185 | self._nodes = [] 186 | self._logCaptures = [] 187 | } 188 | 189 | func makeLogCapture(name: String, settings: inout SWIMNIO.Settings) { 190 | var captureSettings = LogCapture.Settings() 191 | self.configureLogCapture(settings: &captureSettings) 192 | let capture = LogCapture(settings: captureSettings) 193 | 194 | settings.logger = capture.logger(label: name) 195 | 196 | self._logCaptures.append(capture) 197 | } 198 | } 199 | 200 | // ==== ---------------------------------------------------------------------------------------------------------------- 201 | // MARK: Captured Logs 202 | 203 | extension BaseClusteredXCTestCase { 204 | public func capturedLogs(of node: Node) -> LogCapture { 205 | guard let index = self._nodes.firstIndex(of: node) else { 206 | fatalError("No such node: [\(node)] in [\(self._nodes)]!") 207 | } 208 | 209 | return self._logCaptures[index] 210 | } 211 | 212 | public func printCapturedLogs(of node: Node) { 213 | print("------------------------------------- \(node) ------------------------------------------------") 214 | self.capturedLogs(of: node).printLogs() 215 | print( 216 | "========================================================================================================================" 217 | ) 218 | } 219 | 220 | public func printAllCapturedLogs() { 221 | for node in self._nodes { 222 | self.printCapturedLogs(of: node) 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /Tests/SWIMTests/HeapTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftNIO open source project 4 | // 5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.md for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import XCTest 16 | 17 | @testable import SWIM 18 | 19 | public func getRandomNumbers(count: Int) -> [UInt8] { 20 | var values: [UInt8] = .init(repeating: 0, count: count) 21 | let fd = open("/dev/urandom", O_RDONLY) 22 | precondition(fd >= 0) 23 | defer { 24 | close(fd) 25 | } 26 | _ = values.withUnsafeMutableBytes { ptr in 27 | read(fd, ptr.baseAddress!, ptr.count) 28 | } 29 | return values 30 | } 31 | 32 | class HeapTests: XCTestCase { 33 | func testSimple() throws { 34 | var h = Heap(type: .maxHeap) 35 | h.append(1) 36 | h.append(3) 37 | h.append(2) 38 | XCTAssertEqual(3, h.removeRoot()) 39 | XCTAssertTrue(h.checkHeapProperty()) 40 | } 41 | 42 | func testSortedDesc() throws { 43 | var maxHeap = Heap(type: .maxHeap) 44 | var minHeap = Heap(type: .minHeap) 45 | 46 | let input = [16, 14, 10, 9, 8, 7, 4, 3, 2, 1] 47 | input.forEach { 48 | minHeap.append($0) 49 | maxHeap.append($0) 50 | XCTAssertTrue(minHeap.checkHeapProperty()) 51 | XCTAssertTrue(maxHeap.checkHeapProperty()) 52 | } 53 | var minHeapInputPtr = input.count - 1 54 | var maxHeapInputPtr = 0 55 | while let maxE = maxHeap.removeRoot(), let minE = minHeap.removeRoot() { 56 | XCTAssertEqual(maxE, input[maxHeapInputPtr], "\(maxHeap.debugDescription)") 57 | XCTAssertEqual(minE, input[minHeapInputPtr]) 58 | maxHeapInputPtr += 1 59 | minHeapInputPtr -= 1 60 | XCTAssertTrue(minHeap.checkHeapProperty(), "\(minHeap.debugDescription)") 61 | XCTAssertTrue(maxHeap.checkHeapProperty()) 62 | } 63 | XCTAssertEqual(-1, minHeapInputPtr) 64 | XCTAssertEqual(input.count, maxHeapInputPtr) 65 | } 66 | 67 | func testSortedAsc() throws { 68 | var maxHeap = Heap(type: .maxHeap) 69 | var minHeap = Heap(type: .minHeap) 70 | 71 | let input = Array([16, 14, 10, 9, 8, 7, 4, 3, 2, 1].reversed()) 72 | input.forEach { 73 | minHeap.append($0) 74 | maxHeap.append($0) 75 | } 76 | var minHeapInputPtr = 0 77 | var maxHeapInputPtr = input.count - 1 78 | while let maxE = maxHeap.removeRoot(), let minE = minHeap.removeRoot() { 79 | XCTAssertEqual(maxE, input[maxHeapInputPtr]) 80 | XCTAssertEqual(minE, input[minHeapInputPtr]) 81 | maxHeapInputPtr -= 1 82 | minHeapInputPtr += 1 83 | } 84 | XCTAssertEqual(input.count, minHeapInputPtr) 85 | XCTAssertEqual(-1, maxHeapInputPtr) 86 | } 87 | 88 | func testSortedCustom() throws { 89 | struct Test: Equatable { 90 | let x: Int 91 | } 92 | 93 | var maxHeap = Heap(of: Test.self) { 94 | $0.x > $1.x 95 | } 96 | var minHeap = Heap(of: Test.self) { 97 | $0.x < $1.x 98 | } 99 | 100 | let input = Array([16, 14, 10, 9, 8, 7, 4, 3, 2, 1].reversed().map { Test(x: $0) }) 101 | input.forEach { 102 | minHeap.append($0) 103 | maxHeap.append($0) 104 | } 105 | var minHeapInputPtr = 0 106 | var maxHeapInputPtr = input.count - 1 107 | while let maxE = maxHeap.removeRoot(), let minE = minHeap.removeRoot() { 108 | XCTAssertEqual(maxE, input[maxHeapInputPtr]) 109 | XCTAssertEqual(minE, input[minHeapInputPtr]) 110 | maxHeapInputPtr -= 1 111 | minHeapInputPtr += 1 112 | } 113 | XCTAssertEqual(input.count, minHeapInputPtr) 114 | XCTAssertEqual(-1, maxHeapInputPtr) 115 | } 116 | 117 | func testAddAndRemoveRandomNumbers() throws { 118 | var maxHeap = Heap(type: .maxHeap) 119 | var minHeap = Heap(type: .minHeap) 120 | var maxHeapLast = UInt8.max 121 | var minHeapLast = UInt8.min 122 | 123 | let N = 100 124 | 125 | for n in getRandomNumbers(count: N) { 126 | maxHeap.append(n) 127 | minHeap.append(n) 128 | XCTAssertTrue(maxHeap.checkHeapProperty(), maxHeap.debugDescription) 129 | XCTAssertTrue(minHeap.checkHeapProperty(), maxHeap.debugDescription) 130 | 131 | XCTAssertEqual(Array(minHeap.sorted()), Array(minHeap)) 132 | XCTAssertEqual(Array(maxHeap.sorted().reversed()), Array(maxHeap)) 133 | } 134 | 135 | for _ in 0..(type: .maxHeap, storage: [84, 22, 19, 21, 3, 10, 6, 5, 20])! 178 | _ = h.remove(value: 10) 179 | XCTAssertTrue(h.checkHeapProperty(), "\(h.debugDescription)") 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Tests/SWIMTests/SWIMMetricsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import Dispatch 17 | import Metrics 18 | import SWIMTestKit 19 | import XCTest 20 | 21 | @testable import CoreMetrics 22 | @testable import SWIM 23 | 24 | final class SWIMMetricsTests: XCTestCase { 25 | let myselfNode = ClusterMembership.Node(protocol: "test", host: "127.0.0.1", port: 7001, uid: 1111) 26 | let secondNode = ClusterMembership.Node(protocol: "test", host: "127.0.0.1", port: 7002, uid: 2222) 27 | let thirdNode = ClusterMembership.Node(protocol: "test", host: "127.0.0.1", port: 7003, uid: 3333) 28 | let fourthNode = ClusterMembership.Node(protocol: "test", host: "127.0.0.1", port: 7004, uid: 4444) 29 | let fifthNode = ClusterMembership.Node(protocol: "test", host: "127.0.0.1", port: 7005, uid: 5555) 30 | 31 | var myself: TestPeer! 32 | var second: TestPeer! 33 | var third: TestPeer! 34 | var fourth: TestPeer! 35 | var fifth: TestPeer! 36 | 37 | var testMetrics: TestMetrics! 38 | 39 | override func setUp() { 40 | super.setUp() 41 | self.myself = TestPeer(node: self.myselfNode) 42 | self.second = TestPeer(node: self.secondNode) 43 | self.third = TestPeer(node: self.thirdNode) 44 | self.fourth = TestPeer(node: self.fourthNode) 45 | self.fifth = TestPeer(node: self.fifthNode) 46 | 47 | self.testMetrics = TestMetrics() 48 | MetricsSystem.bootstrapInternal(self.testMetrics) 49 | } 50 | 51 | override func tearDown() { 52 | super.tearDown() 53 | self.myself = nil 54 | self.second = nil 55 | self.third = nil 56 | self.fourth = nil 57 | self.fifth = nil 58 | 59 | MetricsSystem.bootstrapInternal(NOOPMetricsHandler.instance) 60 | } 61 | 62 | // ==== ------------------------------------------------------------------------------------------------------------ 63 | // MARK: Metrics tests 64 | 65 | let alive = [("status", "alive")] 66 | let unreachable = [("status", "unreachable")] 67 | let dead = [("status", "dead")] 68 | 69 | func test_members_becoming_suspect() { 70 | var settings = SWIM.Settings() 71 | settings.unreachability = .enabled 72 | var swim = SWIM.Instance(settings: settings, myself: self.myself) 73 | 74 | self.expectMembership(swim, alive: 1, unreachable: 0, totalDead: 0) 75 | 76 | _ = swim.addMember(self.second, status: .alive(incarnation: 0)) 77 | self.expectMembership(swim, alive: 2, unreachable: 0, totalDead: 0) 78 | 79 | _ = swim.addMember(self.third, status: .alive(incarnation: 0)) 80 | self.expectMembership(swim, alive: 3, unreachable: 0, totalDead: 0) 81 | 82 | _ = swim.addMember(self.fourth, status: .alive(incarnation: 0)) 83 | _ = swim.onPeriodicPingTick() 84 | self.expectMembership(swim, alive: 4, unreachable: 0, totalDead: 0) 85 | 86 | for _ in 0..<10 { 87 | _ = swim.onPingResponse( 88 | response: .timeout( 89 | target: self.second, 90 | pingRequestOrigin: nil, 91 | timeout: .seconds(1), 92 | sequenceNumber: 0 93 | ), 94 | pingRequestOrigin: nil, 95 | pingRequestSequenceNumber: nil 96 | ) 97 | _ = swim.onPingRequestResponse(.nack(target: self.third, sequenceNumber: 0), pinged: self.second) 98 | } 99 | expectMembership(swim, suspect: 1) 100 | 101 | for _ in 0..<10 { 102 | _ = swim.onPingResponse( 103 | response: .timeout(target: self.third, pingRequestOrigin: nil, timeout: .seconds(1), sequenceNumber: 0), 104 | pingRequestOrigin: nil, 105 | pingRequestSequenceNumber: nil 106 | ) 107 | } 108 | expectMembership(swim, suspect: 2) 109 | } 110 | 111 | enum DowningMode { 112 | case unreachableFirst 113 | case deadImmediately 114 | } 115 | 116 | func test_members_becoming_dead() { 117 | self.shared_members(mode: .deadImmediately) 118 | } 119 | 120 | func test_members_becoming_unreachable() { 121 | self.shared_members(mode: .unreachableFirst) 122 | } 123 | 124 | func shared_members(mode: DowningMode) { 125 | var settings = SWIM.Settings() 126 | switch mode { 127 | case .unreachableFirst: 128 | settings.unreachability = .enabled 129 | case .deadImmediately: 130 | settings.unreachability = .disabled 131 | } 132 | var mockTime = DispatchTime.now() 133 | settings.timeSourceNow = { mockTime } 134 | var swim = SWIM.Instance(settings: settings, myself: self.myself) 135 | 136 | self.expectMembership(swim, alive: 1, unreachable: 0, totalDead: 0) 137 | 138 | _ = swim.addMember(self.second, status: .alive(incarnation: 0)) 139 | self.expectMembership(swim, alive: 2, unreachable: 0, totalDead: 0) 140 | 141 | _ = swim.addMember(self.third, status: .alive(incarnation: 0)) 142 | self.expectMembership(swim, alive: 3, unreachable: 0, totalDead: 0) 143 | 144 | _ = swim.addMember(self.fourth, status: .alive(incarnation: 0)) 145 | _ = swim.onPeriodicPingTick() 146 | self.expectMembership(swim, alive: 4, unreachable: 0, totalDead: 0) 147 | 148 | let totalMembers = 4 149 | 150 | for _ in 0..<10 { 151 | _ = swim.onPingResponse( 152 | response: .timeout( 153 | target: self.second, 154 | pingRequestOrigin: nil, 155 | timeout: .seconds(1), 156 | sequenceNumber: 0 157 | ), 158 | pingRequestOrigin: nil, 159 | pingRequestSequenceNumber: nil 160 | ) 161 | mockTime = mockTime + DispatchTimeInterval.seconds(120) 162 | _ = swim.onPeriodicPingTick() 163 | } 164 | let (expectedUnreachables1, expectedDeads1): (Int, Int) 165 | switch mode { 166 | case .unreachableFirst: (expectedUnreachables1, expectedDeads1) = (1, 0) 167 | case .deadImmediately: (expectedUnreachables1, expectedDeads1) = (0, 1) 168 | } 169 | self.expectMembership( 170 | swim, 171 | alive: totalMembers - expectedDeads1 - expectedUnreachables1, 172 | unreachable: expectedUnreachables1, 173 | totalDead: expectedDeads1 174 | ) 175 | 176 | for _ in 0..<10 { 177 | _ = swim.onPingResponse( 178 | response: .timeout(target: self.third, pingRequestOrigin: nil, timeout: .seconds(1), sequenceNumber: 0), 179 | pingRequestOrigin: nil, 180 | pingRequestSequenceNumber: nil 181 | ) 182 | mockTime = mockTime + DispatchTimeInterval.seconds(120) 183 | _ = swim.onPeriodicPingTick() 184 | } 185 | let (expectedUnreachables2, expectedDeads2): (Int, Int) 186 | switch mode { 187 | case .unreachableFirst: (expectedUnreachables2, expectedDeads2) = (2, 0) 188 | case .deadImmediately: (expectedUnreachables2, expectedDeads2) = (0, 2) 189 | } 190 | self.expectMembership( 191 | swim, 192 | alive: totalMembers - expectedDeads2 - expectedUnreachables2, 193 | unreachable: expectedUnreachables2, 194 | totalDead: expectedDeads2 195 | ) 196 | 197 | if mode == .unreachableFirst { 198 | _ = swim.confirmDead(peer: self.second) 199 | self.expectMembership( 200 | swim, 201 | alive: totalMembers - expectedDeads2 - expectedUnreachables2, 202 | unreachable: expectedUnreachables2 - 1, 203 | totalDead: expectedDeads2 + 1 204 | ) 205 | 206 | let gotRemovedDeadTombstones = try! self.testMetrics.expectRecorder( 207 | swim.metrics.removedDeadMemberTombstones 208 | ).lastValue! 209 | XCTAssertEqual(gotRemovedDeadTombstones, Double(expectedDeads2 + 1)) 210 | } 211 | } 212 | 213 | func test_lha_adjustment() { 214 | let settings = SWIM.Settings() 215 | var swim = SWIM.Instance(settings: settings, myself: self.myself) 216 | 217 | _ = swim.addMember(self.second, status: .alive(incarnation: 0)) 218 | _ = swim.addMember(self.third, status: .alive(incarnation: 0)) 219 | 220 | XCTAssertEqual(try! self.testMetrics.expectRecorder(swim.metrics.localHealthMultiplier).lastValue, Double(0)) 221 | 222 | swim.adjustLHMultiplier(.failedProbe) 223 | XCTAssertEqual(try! self.testMetrics.expectRecorder(swim.metrics.localHealthMultiplier).lastValue, Double(1)) 224 | 225 | swim.adjustLHMultiplier(.failedProbe) 226 | XCTAssertEqual(try! self.testMetrics.expectRecorder(swim.metrics.localHealthMultiplier).lastValue, Double(2)) 227 | 228 | swim.adjustLHMultiplier(.successfulProbe) 229 | XCTAssertEqual(try! self.testMetrics.expectRecorder(swim.metrics.localHealthMultiplier).lastValue, Double(1)) 230 | } 231 | } 232 | 233 | // ==== ---------------------------------------------------------------------------------------------------------------- 234 | // MARK: Assertions 235 | 236 | extension SWIMMetricsTests { 237 | private func expectMembership( 238 | _ swim: SWIM.Instance, 239 | suspect: Int, 240 | file: StaticString = #file, 241 | line: UInt = #line 242 | ) { 243 | let m: SWIM.Metrics = swim.metrics 244 | 245 | let gotSuspect: Double? = try! self.testMetrics.expectRecorder(m.membersSuspect).lastValue 246 | XCTAssertEqual( 247 | gotSuspect, 248 | Double(suspect), 249 | """ 250 | Expected \(suspect) [alive] members, was: \(String(reflecting: gotSuspect)); Members: 251 | \(swim.members.map(\.description).joined(separator: "\n")) 252 | """, 253 | file: file, 254 | line: line 255 | ) 256 | } 257 | 258 | private func expectMembership( 259 | _ swim: SWIM.Instance, 260 | alive: Int, 261 | unreachable: Int, 262 | totalDead: Int, 263 | file: StaticString = #file, 264 | line: UInt = #line 265 | ) { 266 | let m: SWIM.Metrics = swim.metrics 267 | 268 | let gotAlive: Double? = try! self.testMetrics.expectRecorder(m.membersAlive).lastValue 269 | XCTAssertEqual( 270 | gotAlive, 271 | Double(alive), 272 | """ 273 | Expected \(alive) [alive] members, was: \(String(reflecting: gotAlive)); Members: 274 | \(swim.members.map(\.description).joined(separator: "\n")) 275 | """, 276 | file: file, 277 | line: line 278 | ) 279 | 280 | let gotUnreachable: Double? = try! self.testMetrics.expectRecorder(m.membersUnreachable).lastValue 281 | XCTAssertEqual( 282 | gotUnreachable, 283 | Double(unreachable), 284 | """ 285 | Expected \(unreachable) [unreachable] members, was: \(String(reflecting: gotUnreachable)); Members: 286 | \(swim.members.map(\.description).joined(separator: "\n"))) 287 | """, 288 | file: file, 289 | line: line 290 | ) 291 | 292 | let gotTotalDead: Int64? = try! self.testMetrics.expectCounter(m.membersTotalDead).totalValue 293 | XCTAssertEqual( 294 | gotTotalDead, 295 | Int64(totalDead), 296 | """ 297 | Expected \(totalDead) [dead] members, was: \(String(reflecting: gotTotalDead)); Members: 298 | \(swim.members.map(\.description).joined(separator: "\n")) 299 | """, 300 | file: file, 301 | line: line 302 | ) 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /Tests/SWIMTests/SWIMSettingsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import XCTest 17 | 18 | @testable import SWIM 19 | 20 | final class SWIMSettingsTests: XCTestCase { 21 | func test_gossipedEnoughTimes() { 22 | let settings = SWIM.Settings() 23 | 24 | let node = ClusterMembership.Node(protocol: "test", host: "127.0.0.1", port: 7001, uid: 1111) 25 | let member = SWIM.Member(peer: TestPeer(node: node), status: .alive(incarnation: 0), protocolPeriod: 0) 26 | var g = SWIM.Gossip(member: member, numberOfTimesGossiped: 0) 27 | 28 | var members = 0 29 | 30 | // just 1 member, means no other peers thus we dont have to gossip ever 31 | members = 1 32 | g.numberOfTimesGossiped = 0 33 | XCTAssertEqual(settings.gossip.gossipedEnoughTimes(g, members: members), false) 34 | g.numberOfTimesGossiped = 1 35 | XCTAssertEqual(settings.gossip.gossipedEnoughTimes(g, members: members), false) 36 | 37 | members = 2 38 | g.numberOfTimesGossiped = 0 39 | for _ in 0...3 { 40 | XCTAssertEqual(settings.gossip.gossipedEnoughTimes(g, members: members), false) 41 | g.numberOfTimesGossiped += 1 42 | } 43 | 44 | members = 10 45 | g.numberOfTimesGossiped = 0 46 | for _ in 0...9 { 47 | XCTAssertEqual(settings.gossip.gossipedEnoughTimes(g, members: members), false) 48 | g.numberOfTimesGossiped += 1 49 | } 50 | 51 | members = 50 52 | g.numberOfTimesGossiped = 0 53 | for _ in 0...16 { 54 | XCTAssertEqual(settings.gossip.gossipedEnoughTimes(g, members: members), false) 55 | g.numberOfTimesGossiped += 1 56 | } 57 | 58 | members = 200 59 | g.numberOfTimesGossiped = 0 60 | for _ in 0...21 { 61 | XCTAssertEqual(settings.gossip.gossipedEnoughTimes(g, members: members), false) 62 | g.numberOfTimesGossiped += 1 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/SWIMTests/TestPeer.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Cluster Membership open source project 4 | // 5 | // Copyright (c) 2018-2022 Apple Inc. and the Swift Cluster Membership project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Cluster Membership project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ClusterMembership 16 | import Dispatch 17 | import XCTest 18 | 19 | @testable import SWIM 20 | 21 | final class TestPeer: Hashable, SWIMPeer, SWIMPingOriginPeer, SWIMPingRequestOriginPeer, CustomStringConvertible { 22 | var swimNode: Node 23 | 24 | let semaphore = DispatchSemaphore(value: 1) 25 | var messages: [TestPeer.Message] = [] 26 | 27 | enum Message { 28 | case ping( 29 | payload: SWIM.GossipPayload, 30 | origin: TestPeer, 31 | timeout: Duration, 32 | sequenceNumber: SWIM.SequenceNumber, 33 | continuation: CheckedContinuation, Error> 34 | ) 35 | case pingReq( 36 | target: TestPeer, 37 | payload: SWIM.GossipPayload, 38 | origin: TestPeer, 39 | timeout: Duration, 40 | sequenceNumber: SWIM.SequenceNumber, 41 | continuation: CheckedContinuation, Error> 42 | ) 43 | case ack( 44 | target: TestPeer, 45 | incarnation: SWIM.Incarnation, 46 | payload: SWIM.GossipPayload, 47 | sequenceNumber: SWIM.SequenceNumber 48 | ) 49 | case nack( 50 | target: TestPeer, 51 | sequenceNumber: SWIM.SequenceNumber 52 | ) 53 | } 54 | 55 | init(node: Node) { 56 | self.swimNode = node 57 | } 58 | 59 | func ping( 60 | payload: SWIM.GossipPayload, 61 | from pingOrigin: TestPeer, 62 | timeout: Duration, 63 | sequenceNumber: SWIM.SequenceNumber 64 | ) async throws -> SWIM.PingResponse { 65 | self.semaphore.wait() 66 | defer { self.semaphore.signal() } 67 | 68 | return try await withCheckedThrowingContinuation { continuation in 69 | self.messages.append( 70 | .ping( 71 | payload: payload, 72 | origin: pingOrigin, 73 | timeout: timeout, 74 | sequenceNumber: sequenceNumber, 75 | continuation: continuation 76 | ) 77 | ) 78 | } 79 | } 80 | 81 | func pingRequest( 82 | target: TestPeer, 83 | payload: SWIM.GossipPayload, 84 | from origin: TestPeer, 85 | timeout: Duration, 86 | sequenceNumber: SWIM.SequenceNumber 87 | ) async throws -> SWIM.PingResponse { 88 | self.semaphore.wait() 89 | defer { self.semaphore.signal() } 90 | 91 | return try await withCheckedThrowingContinuation { continuation in 92 | self.messages.append( 93 | .pingReq( 94 | target: target, 95 | payload: payload, 96 | origin: origin, 97 | timeout: timeout, 98 | sequenceNumber: sequenceNumber, 99 | continuation: continuation 100 | ) 101 | ) 102 | } 103 | } 104 | 105 | func ack( 106 | acknowledging sequenceNumber: SWIM.SequenceNumber, 107 | target: TestPeer, 108 | incarnation: SWIM.Incarnation, 109 | payload: SWIM.GossipPayload 110 | ) { 111 | self.semaphore.wait() 112 | defer { self.semaphore.signal() } 113 | 114 | self.messages.append( 115 | .ack(target: target, incarnation: incarnation, payload: payload, sequenceNumber: sequenceNumber) 116 | ) 117 | } 118 | 119 | func nack( 120 | acknowledging sequenceNumber: SWIM.SequenceNumber, 121 | target: TestPeer 122 | ) { 123 | self.semaphore.wait() 124 | defer { self.semaphore.signal() } 125 | 126 | self.messages.append(.nack(target: target, sequenceNumber: sequenceNumber)) 127 | } 128 | 129 | func hash(into hasher: inout Hasher) { 130 | hasher.combine(self.node) 131 | } 132 | 133 | static func == (lhs: TestPeer, rhs: TestPeer) -> Bool { 134 | if lhs === rhs { 135 | return true 136 | } 137 | if type(of: lhs) != type(of: rhs) { 138 | return false 139 | } 140 | if lhs.node != rhs.node { 141 | return false 142 | } 143 | return true 144 | } 145 | 146 | var description: String { 147 | "TestPeer(\(self.swimNode))" 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /dev/git.commit.template: -------------------------------------------------------------------------------- 1 | # One line description of your change 2 | 3 | **Motivation:** 4 | 5 | # Explain here the context and why you're making that change. What is the problem you're trying to solve? 6 | 7 | **Modifications:** 8 | 9 | # Describe the modifications you've done. 10 | 11 | **Result:** 12 | 13 | - Resolves # 14 | --------------------------------------------------------------------------------