├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── actions │ ├── build-docs │ │ └── action.yml │ ├── ci │ │ └── action.yml │ ├── configure-rebar │ │ └── action.yml │ ├── install-erlang │ │ └── action.yml │ ├── publish-docs │ │ └── action.yml │ └── publish │ │ └── action.yml ├── pull_request_template.md ├── scripts │ └── configure_rebar3.sh └── workflows │ ├── ci.yml │ ├── lint-pr-title.yml │ ├── manual-publish-docs.yml │ ├── manual-publish.yml │ ├── release-please.yml │ └── stale.yml ├── .gitignore ├── .release-please-manifest.json ├── .sdk_metadata.json ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE.txt ├── Makefile ├── README.md ├── SECURITY.md ├── doc └── overview.edoc ├── priv ├── flags-all-properties.yaml ├── flags-from-file-no-flags.json ├── flags-from-file-no-segments.json ├── flags-from-file-simple.json ├── flags-from-file-simple.yaml ├── flags-from-file.json ├── flags-segments-put-data-another1.json └── flags-segments-put-data.json ├── rebar.config ├── release-please-config.json ├── src ├── ldclient.app.src ├── ldclient.erl ├── ldclient_app.erl ├── ldclient_attribute_reference.erl ├── ldclient_backoff.erl ├── ldclient_clause.erl ├── ldclient_config.erl ├── ldclient_context.erl ├── ldclient_context_cache.erl ├── ldclient_context_filter.erl ├── ldclient_eval.erl ├── ldclient_eval_reason.erl ├── ldclient_event.erl ├── ldclient_event_dispatch.erl ├── ldclient_event_dispatch_httpc.erl ├── ldclient_event_process_server.erl ├── ldclient_event_server.erl ├── ldclient_event_sup.erl ├── ldclient_flag.erl ├── ldclient_flagbuilder.erl ├── ldclient_headers.erl ├── ldclient_http.erl ├── ldclient_http_options.erl ├── ldclient_instance.erl ├── ldclient_instance_sup.erl ├── ldclient_rollout.erl ├── ldclient_rule.erl ├── ldclient_segment.erl ├── ldclient_storage_cache.erl ├── ldclient_storage_cache_server.erl ├── ldclient_storage_cache_sup.erl ├── ldclient_storage_engine.erl ├── ldclient_storage_ets.erl ├── ldclient_storage_ets_server.erl ├── ldclient_storage_ets_sup.erl ├── ldclient_storage_map.erl ├── ldclient_storage_map_server.erl ├── ldclient_storage_map_sup.erl ├── ldclient_storage_redis.erl ├── ldclient_storage_redis_server.erl ├── ldclient_storage_redis_sup.erl ├── ldclient_sup.erl ├── ldclient_targets.erl ├── ldclient_testdata.erl ├── ldclient_time.erl ├── ldclient_update_file_server.erl ├── ldclient_update_null_server.erl ├── ldclient_update_poll_server.erl ├── ldclient_update_processor_state.erl ├── ldclient_update_requestor.erl ├── ldclient_update_requestor_httpc.erl ├── ldclient_update_stream_server.erl ├── ldclient_update_sup.erl ├── ldclient_update_testdata_server.erl ├── ldclient_updater.erl ├── ldclient_user.erl └── ldclient_yaml_mapper.erl ├── test-redis ├── ldclient_storage_cache_SUITE.erl ├── ldclient_storage_redis_SUITE.erl ├── ldclient_stream_redis_SUITE.erl └── ldclient_stream_redis_online_SUITE.erl ├── test-service ├── README.md ├── rebar.config ├── src │ ├── ts_app.app.src │ ├── ts_client_handler.erl │ ├── ts_command_handler.erl │ ├── ts_command_params.erl │ ├── ts_command_request_handler.erl │ ├── ts_sdk_config_params.erl │ ├── ts_server.erl │ ├── ts_server_sup.erl │ ├── ts_service_params.erl │ └── ts_service_request_handler.erl └── testharness-suppressions.txt ├── test-tls └── ldclient_tls_options_SUITE.erl ├── test-usage └── ldclient_usage.erl └── test ├── http_server.app ├── http_server ├── http_server.erl ├── http_server_sse_handler.erl ├── http_server_sup.erl └── http_server_timeout_once.erl ├── ldclient_attribute_reference_SUITE.erl ├── ldclient_backoff_SUITE.erl ├── ldclient_clause_SUITE.erl ├── ldclient_config_SUITE.erl ├── ldclient_context_SUITE.erl ├── ldclient_context_filter_SUITE.erl ├── ldclient_eval_SUITE.erl ├── ldclient_event_dispatch_httpc_SUITE.erl ├── ldclient_event_dispatch_test.erl ├── ldclient_events_SUITE.erl ├── ldclient_file_SUITE.erl ├── ldclient_headers_SUITE.erl ├── ldclient_http_SUITE.erl ├── ldclient_http_options_SUITE.erl ├── ldclient_parse_SUITE.erl ├── ldclient_poll_SUITE.erl ├── ldclient_rollout_SUITE.erl ├── ldclient_rollout_randomization_consistency_SUITE.erl ├── ldclient_segment_SUITE.erl ├── ldclient_start_SUITE.erl ├── ldclient_storage_ets_SUITE.erl ├── ldclient_storage_map_SUITE.erl ├── ldclient_stream_SUITE.erl ├── ldclient_stream_online_SUITE.erl ├── ldclient_targets_SUITE.erl ├── ldclient_test_utils.erl ├── ldclient_testdata_SUITE.erl ├── ldclient_update_requestor_httpc_SUITE.erl ├── ldclient_update_requestor_test.erl ├── ldclient_user_SUITE.erl └── ldclient_yaml_mapper_SUITE.erl /.gitattributes: -------------------------------------------------------------------------------- 1 | erlang.mk -diff 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is this a support request?** 11 | This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the SDK code. If you're not sure whether the problem you are having is specifically related to the SDK, or to the LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to investigate the problem and will consult the SDK team if necessary. You can submit a support request by going [here](https://support.launchdarkly.com/hc/en-us/requests/new) or by emailing support@launchdarkly.com. 12 | 13 | Note that issues filed on this issue tracker are publicly accessible. Do not provide any private account information on your issues. If your problem is specific to your account, you should submit a support request as described above. 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To reproduce** 19 | Steps to reproduce the behavior. 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Logs** 25 | If applicable, add any log output related to your problem. 26 | 27 | **SDK version** 28 | The version of this SDK that you are using. 29 | 30 | **Language version, developer tools** 31 | For instance, Go 1.11 or Ruby 2.5.3. If you are using a language that requires a separate compiler, such as C, please include the name and version of the compiler too. 32 | 33 | **OS/platform** 34 | For instance, Ubuntu 16.04, Windows 10, or Android 4.0.3. If your code is running in a browser, please also include the browser type and version. 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support request 4 | url: https://support.launchdarkly.com/hc/en-us/requests/new 5 | about: File your support requests with LaunchDarkly's support team 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I would love to see the SDK [...does something new...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/build-docs/action.yml: -------------------------------------------------------------------------------- 1 | # Use a step like this to build documentation. 2 | name: Build Documentation 3 | description: 'Build Documentation.' 4 | 5 | runs: 6 | using: composite 7 | steps: 8 | - name: Build Documentation 9 | shell: bash 10 | run: make doc 11 | -------------------------------------------------------------------------------- /.github/actions/ci/action.yml: -------------------------------------------------------------------------------- 1 | # This is a composite to allow sharing these steps into other workflows. 2 | # For instance it could be used by regular CI as well as the release process. 3 | 4 | name: CI Workflow 5 | description: "Shared CI workflow." 6 | inputs: 7 | run_tests: 8 | description: "If true, run unit tests, otherwise skip them." 9 | required: false 10 | default: "true" 11 | github_token: 12 | description: "Github token used to access contract test harness." 13 | required: true 14 | 15 | runs: 16 | using: composite 17 | steps: 18 | - run: make compile 19 | shell: bash 20 | - uses: supercharge/redis-github-action@ea9b21c6ecece47bd99595c532e481390ea0f044 21 | - run: make ci-tests 22 | shell: bash 23 | - uses: ./.github/actions/build-docs 24 | - run: make dialyze 25 | shell: bash 26 | - run: make build-contract-tests 27 | shell: bash 28 | - name: Start contract test service 29 | shell: bash 30 | run: make start-contract-test-service-bg 31 | - uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.1.0 32 | with: 33 | test_service_port: 8000 34 | token: ${{ inputs.github_token }} 35 | extra_params: '--skip-from=./test-service/testharness-suppressions.txt' 36 | -------------------------------------------------------------------------------- /.github/actions/configure-rebar/action.yml: -------------------------------------------------------------------------------- 1 | name: Configure Rebar3 2 | description: 'Configure publishing token for Rebar3' 3 | inputs: 4 | aws_assume_role: 5 | description: 'The ARN of an AWS IAM role to assume. Used to auth with AWS to upload results to S3.' 6 | required: true 7 | 8 | runs: 9 | using: composite 10 | steps: 11 | - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.0.0 12 | name: 'Get the hex publishing token' 13 | with: 14 | aws_assume_role: ${{ inputs.aws_assume_role }} 15 | ssm_parameter_pairs: '/production/common/releasing/hex/api_key = HEX_AUTH_TOKEN' 16 | - name: Configure rebar3 17 | shell: bash 18 | run: ./.github/scripts/configure_rebar3.sh 19 | env: 20 | HEX_AUTH_TOKEN: ${{ env.HEX_AUTH_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/actions/install-erlang/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Install Erlang OTP and rebar3' 2 | description: 'Installs a specific version of Erlang OTP and rebar3' 3 | inputs: 4 | otp_version: 5 | description: 'The OTP version to install' 6 | required: false 7 | default: '25.3.2.19' 8 | rebar_version: 9 | description: 'The rebar3 version to install' 10 | required: false 11 | default: '174fd9070195443d693d444ecd1f2b7aa91661fe' # 3.18.0 12 | runs: 13 | using: "composite" 14 | steps: 15 | 16 | - name: Install kerl 17 | shell: bash 18 | run: | 19 | curl -s https://raw.githubusercontent.com/kerl/kerl/master/kerl > /tmp/kerl 20 | chmod a+x /tmp/kerl 21 | 22 | - name: Install Erlang OTP and rebar3 23 | shell: bash 24 | run: | 25 | # Build and install Erlang 26 | /tmp/kerl build ${{ inputs.otp_version }} ${{ inputs.otp_version }} 27 | /tmp/kerl install ${{ inputs.otp_version }} ~/otp/${{ inputs.otp_version }} 28 | # This will make the erlang version available to the current shell. 29 | . ~/otp/${{ inputs.otp_version }}/activate 30 | # Persist to the github actions path for use in future steps. 31 | echo "$HOME/otp/${{ inputs.otp_version }}/bin" >> "$GITHUB_PATH" 32 | 33 | # Install specific rebar3 version 34 | cd ~/ 35 | git clone https://github.com/erlang/rebar3.git 36 | cd rebar3 37 | git checkout ${{ matrix.versions.rebar }} 38 | ./bootstrap 39 | ./rebar3 local install 40 | 41 | # Add to the path for use in this step. 42 | export PATH=$HOME/otp/${{ inputs.otp_version }}/.cache/rebar3/bin:$PATH 43 | # Persist to the github actions path for use in future steps. 44 | echo "$HOME/otp/${{ inputs.otp_version }}/.cache/rebar3/bin" >> "$GITHUB_PATH" 45 | 46 | # Verify installation 47 | echo "Installed Erlang version:" 48 | erl -noshell -eval 'io:format("~s~n", [erlang:system_info(otp_release)]), halt().' 49 | echo "Installed rebar3 version:" 50 | rebar3 --version 51 | -------------------------------------------------------------------------------- /.github/actions/publish-docs/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish Documentation 2 | description: 'Publish the documentation to hex.pm and github pages.' 3 | 4 | inputs: 5 | github_token: 6 | description: 'The github token to use for committing' 7 | required: true 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | - uses: launchdarkly/gh-actions/actions/publish-pages@publish-pages-v1.0.1 13 | name: 'Publish to Github pages' 14 | with: 15 | docs_path: doc 16 | github_token: ${{ inputs.github_token }} 17 | - run: rebar3 hex docs 18 | shell: bash 19 | -------------------------------------------------------------------------------- /.github/actions/publish/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | description: 'Publish the package to Hex.pm' 3 | inputs: 4 | dry_run: 5 | description: 'Is this a dry run. If so no package will be published.' 6 | required: true 7 | 8 | runs: 9 | using: composite 10 | steps: 11 | - name: Publish Library 12 | shell: bash 13 | run: rebar3 hex publish --yes 14 | # Do not publish a dry run. 15 | if: ${{ inputs.dry_run == 'false' }} 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Requirements** 2 | 3 | - [ ] I have added test coverage for new or changed functionality 4 | - [ ] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) 5 | - [ ] I have validated my changes against all supported platform versions 6 | 7 | **Related issues** 8 | 9 | Provide links to any issues in this repository or elsewhere relating to this pull request. 10 | 11 | **Describe the solution you've provided** 12 | 13 | Provide a clear and concise description of what you expect to happen. 14 | 15 | **Describe alternatives you've considered** 16 | 17 | Provide a clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | 21 | Add any other context about the pull request here. 22 | -------------------------------------------------------------------------------- /.github/scripts/configure_rebar3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make the directory the configuration should be stored in. 4 | mkdir -p ~/.config/rebar3/ 5 | 6 | # Create rebar config with hex plugin. 7 | cat >~/.config/rebar3/rebar.config <~/.config/rebar3/hex.config <> => 17 | #{username => <<"launchdarkly">>, 18 | api_key => <<"${HEX_AUTH_TOKEN}">>}}. 19 | EOF 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '**.md' #Do not need to run CI for markdown changes. 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - '**.md' 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build-test: 16 | strategy: 17 | # We want to know the status of all OTP versions, not just if one of them fails. 18 | fail-fast: false 19 | matrix: 20 | versions: 21 | - { otp: "21.3.8.24", rebar: "0be8717a4ff5b4a0c3dcef5031fe9833197d861e" } # rebar 3.15.2 22 | - { otp: "25.3.2.19", rebar: "174fd9070195443d693d444ecd1f2b7aa91661fe" } # rebar 3.18.0 23 | - { otp: "27.3.2", rebar: "bde4b54248d16280b2c70a244aca3bb7566e2033" } # rebar 3.23.0 24 | 25 | runs-on: ubuntu-24.04 26 | name: Build and Test - ${{ matrix.versions.otp }} 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Install Erlang and rebar3 34 | uses: ./.github/actions/install-erlang 35 | with: 36 | otp_version: ${{ matrix.versions.otp }} 37 | rebar_version: ${{ matrix.versions.rebar }} 38 | 39 | - uses: ./.github/actions/ci 40 | with: 41 | github_token: ${{ secrets.GITHUB_TOKEN }} 42 | env: 43 | OTP_VER: ${{ matrix.versions.otp }} 44 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Lint PR title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | lint-pr-title: 12 | uses: launchdarkly/gh-actions/.github/workflows/lint-pr-title.yml@main 13 | -------------------------------------------------------------------------------- /.github/workflows/manual-publish-docs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: Publish Documentation 5 | jobs: 6 | build-publish: 7 | runs-on: ubuntu-24.04 8 | # Needed to get tokens during publishing. 9 | permissions: 10 | id-token: write 11 | contents: write # Need to publish github pages. 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Install Erlang and rebar3 16 | uses: ./.github/actions/install-erlang 17 | 18 | - id: build 19 | name: Build Documentation 20 | uses: ./.github/actions/build-docs 21 | 22 | - uses: ./.github/actions/configure-rebar 23 | with: 24 | aws_assume_role: ${{ vars.AWS_ROLE_ARN }} 25 | 26 | - id: publish 27 | name: Publish Documentation 28 | uses: ./.github/actions/publish-docs 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/manual-publish.yml: -------------------------------------------------------------------------------- 1 | name: Manual Publish Package 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | dry_run: 6 | description: 'Is this a dry run. If so no package will be published.' 7 | type: boolean 8 | required: true 9 | 10 | jobs: 11 | manual-build-publish: 12 | runs-on: ubuntu-24.04 13 | # Needed to get tokens during publishing. 14 | permissions: 15 | id-token: write 16 | contents: read 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Install Erlang and rebar3 21 | uses: ./.github/actions/install-erlang 22 | 23 | - id: build-and-test 24 | # Build using the same steps from CI. 25 | name: Build and Test 26 | uses: ./.github/actions/ci 27 | 28 | - uses: ./.github/actions/configure-rebar 29 | with: 30 | aws_assume_role: ${{ vars.AWS_ROLE_ARN }} 31 | if: ${{ !inputs.dry_run }} 32 | 33 | - id: publish 34 | name: Publish Package 35 | uses: ./.github/actions/publish 36 | with: 37 | dry_run: ${{ inputs.dry_run }} 38 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release-package: 10 | runs-on: ubuntu-24.04 11 | permissions: 12 | id-token: write # Needed if using OIDC to get release secrets. 13 | contents: write # Contents and pull-requests are for release-please to make releases. 14 | pull-requests: write 15 | 16 | steps: 17 | - uses: google-github-actions/release-please-action@v3 18 | id: release 19 | with: 20 | command: manifest 21 | token: ${{secrets.GITHUB_TOKEN}} 22 | default-branch: main 23 | 24 | - uses: actions/checkout@v3 25 | if: ${{ steps.release.outputs.releases_created }} 26 | with: 27 | fetch-depth: 0 #If you only need the current version keep this. 28 | 29 | - name: Install Erlang and rebar3 30 | if: ${{ steps.release.outputs.releases_created }} 31 | uses: ./.github/actions/install-erlang 32 | 33 | - uses: ./.github/actions/configure-rebar 34 | with: 35 | aws_assume_role: ${{ vars.AWS_ROLE_ARN }} 36 | if: ${{ steps.release.outputs.releases_created }} 37 | 38 | - uses: ./.github/actions/ci 39 | if: ${{ steps.release.outputs.releases_created }} 40 | 41 | - uses: ./.github/actions/build-docs 42 | if: ${{ steps.release.outputs.releases_created }} 43 | 44 | - uses: ./.github/actions/publish 45 | if: ${{ steps.release.outputs.releases_created }} 46 | with: 47 | dry_run: false 48 | 49 | - uses: ./.github/actions/publish-docs 50 | if: ${{ steps.release.outputs.releases_created }} 51 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | # Happen once per day at 1:30 AM 6 | - cron: '30 1 * * *' 7 | 8 | jobs: 9 | sdk-close-stale: 10 | uses: launchdarkly/gh-actions/.github/workflows/sdk-stale.yml@main 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .erlang.mk 2 | .eunit 3 | .idea 4 | .rebar3 5 | *.iml 6 | *.kdev4 7 | *.o 8 | *.plt 9 | _rel 10 | deps 11 | ebin 12 | erl_crash.dump 13 | logs 14 | test/*.beam 15 | ldclient.d 16 | .DS_Store 17 | _build/ 18 | _checkouts/ 19 | rebar.lock 20 | doc/* 21 | !doc/overview.edoc 22 | *.crashdump 23 | *.beam 24 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "3.7.1" 3 | } -------------------------------------------------------------------------------- /.sdk_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "sdks": { 4 | "erlang-server-sdk": { 5 | "name": "Erlang Server SDK", 6 | "type": "server-side", 7 | "languages": [ 8 | "Erlang", "Elixir" 9 | ], 10 | "userAgents": ["ErlangClient"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Repository Maintainers 2 | * @launchdarkly/team-sdk-erlang 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the LaunchDarkly Server-Side SDK for Erlang/Elixir 2 | 3 | LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/sdk/concepts/contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. 4 | 5 | ## Submitting bug reports and feature requests 6 | 7 | The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/erlang-server-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. 8 | 9 | ## Submitting pull requests 10 | 11 | We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. 12 | 13 | ## Build instructions 14 | 15 | ### Prerequisites 16 | 17 | This project is built with [rebar3](https://www.rebar3.org/) and `make`. To run the unit tests you will need to install Docker and keep the Docker daemon running. 18 | 19 | ### Run all tasks 20 | 21 | From the project root directory: 22 | 23 | ``` 24 | make 25 | ``` 26 | 27 | ### Testing 28 | 29 | To run all unit tests: 30 | 31 | ``` 32 | make tests 33 | ``` 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Catamorphic, Co. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR3 = rebar3 2 | ERL_VERSION = `erl -eval 'io:fwrite("~s~n", [erlang:system_info(otp_release)]), halt().' -noshell` 3 | 4 | all: 5 | @$(REBAR3) do clean, compile, ct, dialyzer 6 | 7 | compile: 8 | @$(REBAR3) compile 9 | 10 | dialyze: 11 | @$(REBAR3) as usage dialyzer 12 | 13 | deps: 14 | @$(REBAR3) get-deps 15 | 16 | rel: all 17 | @$(REBAR3) release 18 | 19 | run: 20 | @$(REBAR3) shell 21 | 22 | doc: 23 | @$(REBAR3) edoc 24 | 25 | tests: 26 | docker run --name ld-test-redis -p 6379:6379 -d redis 27 | @$(REBAR3) ct --dir="test,test-redis" --logdir logs/ct 28 | docker rm --force ld-test-redis 29 | 30 | #This is used in running releaser. In this environment we do not want to run the redis tests. 31 | release-tests: 32 | @$(REBAR3) ct --dir="test" --logdir logs/ct 33 | 34 | #This is used on CircleCI because the Redis Docker container is already started unlike the local tests command 35 | ci-tests: 36 | @$(REBAR3) ct --dir="test,test-redis" --logdir logs/ct 37 | 38 | tls-tests: 39 | @$(REBAR3) ct --dir="test-tls" --logdir logs/ct 40 | 41 | #This is for local debugging if your tests fail and the Redis Docker container is not torn down properly 42 | clean-redis: 43 | docker rm --force ld-test-redis 44 | 45 | colon := : 46 | build-contract-tests: 47 | @mkdir -p test-service/_checkouts 48 | @rm -f $(CURDIR)/test-service/_checkouts/ldclient 49 | @ln -sf $(CURDIR)/ $(CURDIR)/test-service/_checkouts/ldclient 50 | @if [ "$(ERL_VERSION)" -ge "26" ]; then\ 51 | echo Dialyze for OTP 26+;\ 52 | cd test-service && $(REBAR3) as otp26 dialyzer;\ 53 | else\ 54 | cd test-service && $(REBAR3) dialyzer;\ 55 | fi 56 | @cd test-service && $(REBAR3) as prod release 57 | 58 | start-contract-test-service: 59 | @$(CURDIR)/test-service/_build/prod/rel/ts/bin/ts foreground 60 | 61 | start-contract-test-service-bg: 62 | @$(CURDIR)/test-service/_build/prod/rel/ts/bin/ts daemon 63 | 64 | run-contract-tests: 65 | @curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/main/downloader/run.sh \ 66 | | VERSION=v2 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end $(TEST_HARNESS_PARAMS)" sh 67 | 68 | contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests 69 | 70 | .PHONY: all compile dialyze deps rel run doc tests clean-redis circle-tests start-contract-test-service start-contract-test-service-bg run-contract-tests contract-tests 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LaunchDarkly Server-Side SDK for Erlang/Elixir 2 | 3 | [![Actions Status][ci-badge]][ci] 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/launchdarkly_server_sdk/) 5 | ## LaunchDarkly overview 6 | 7 | [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! 8 | 9 | [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) 10 | 11 | ## Supported Erlang/OTP versions 12 | 13 | This version of the LaunchDarkly SDK is compatible with Erlang/OTP 21 or higher. 14 | 15 | ## Getting started 16 | 17 | Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/erlang) for instructions on getting started with using the SDK. 18 | 19 | ## Learn more 20 | 21 | Read our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](http://docs.launchdarkly.com/docs/erlang-sdk-reference). 22 | 23 | ## Testing 24 | 25 | We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all behave correctly. 26 | 27 | ## Contributing 28 | 29 | We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK. 30 | 31 | ## About LaunchDarkly 32 | 33 | * LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: 34 | * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. 35 | * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). 36 | * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. 37 | * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. 38 | * LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. 39 | * Explore LaunchDarkly 40 | * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information 41 | * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides 42 | * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation 43 | * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates 44 | 45 | [ci-badge]: https://github.com/launchdarkly/erlang-server-sdk/actions/workflows/ci.yml/badge.svg 46 | [ci]: https://github.com/launchdarkly/erlang-server-sdk/actions/workflows/ci.yml 47 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting and Fixing Security Issues 2 | 3 | Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty. 4 | 5 | Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors. 6 | -------------------------------------------------------------------------------- /doc/overview.edoc: -------------------------------------------------------------------------------- 1 | @title LaunchDarkly Server-Side SDK for Erlang/Elixir 2 | @doc LaunchDarkly is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. 3 | 4 | This version of the LaunchDarkly SDK is compatible with Erlang/OTP 21 or higher. -------------------------------------------------------------------------------- /priv/flags-all-properties.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | flags: 3 | flag1: 4 | key: flag1 5 | "on": true 6 | version: 1 7 | fallthrough: 8 | variation: 2 9 | variations: 10 | - fall 11 | - "off" 12 | - "on" 13 | flagValues: 14 | my-string-flag-key: "value-1" 15 | my-boolean-flag-key: true 16 | my-integer-flag-key: 3 17 | segments: 18 | seg1: 19 | key: seg1 20 | version: 1 21 | include: ["user1"] 22 | -------------------------------------------------------------------------------- /priv/flags-from-file-no-flags.json: -------------------------------------------------------------------------------- 1 | { 2 | "segments": {} 3 | } 4 | -------------------------------------------------------------------------------- /priv/flags-from-file-no-segments.json: -------------------------------------------------------------------------------- 1 | { 2 | "flags": { 3 | "keep-it-on": { 4 | "clientSide": false, 5 | "debugEventsUntilDate": null, 6 | "deleted": false, 7 | "fallthrough": { 8 | "variation": 0 9 | }, 10 | "key": "keep-it-off", 11 | "offVariation": 1, 12 | "on": true, 13 | "variations": [ 14 | true, 15 | false 16 | ], 17 | "version": 6 18 | }, 19 | "segment-me": { 20 | "clientSide": false, 21 | "deleted": false, 22 | "fallthrough": { 23 | "variation": 0 24 | }, 25 | "key": "segment-me", 26 | "offVariation": 1, 27 | "on": true, 28 | "rules": [ 29 | { 30 | "clauses": [ 31 | { 32 | "attribute": "this-value-does-not-matter", 33 | "negate": false, 34 | "op": "segmentMatch", 35 | "values": [ 36 | "test-included" 37 | ] 38 | } 39 | ], 40 | "id": "ab4a9fb3-7e85-429f-8078-23aa70094540", 41 | "trackEvents": false, 42 | "variation": 1 43 | }, 44 | { 45 | "clauses": [ 46 | { 47 | "attribute": "this-value-does-not-matter", 48 | "negate": true, 49 | "op": "segmentMatch", 50 | "values": [ 51 | "test-included" 52 | ] 53 | } 54 | ], 55 | "id": "489a185d-caaf-4db9-b192-e09e927d070c", 56 | "trackEvents": false, 57 | "variation": 1 58 | } 59 | ], 60 | "variations": [ 61 | true, 62 | false 63 | ], 64 | "version": 5 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /priv/flags-from-file-simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "flagValues": { 3 | "my-string-flag-key": "value-1", 4 | "my-boolean-flag-key": true, 5 | "my-integer-flag-key": 3 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /priv/flags-from-file-simple.yaml: -------------------------------------------------------------------------------- 1 | flagValues: 2 | my-string-flag-key: "value-1" 3 | my-boolean-flag-key: true 4 | my-integer-flag-key: 3 5 | -------------------------------------------------------------------------------- /priv/flags-from-file.json: -------------------------------------------------------------------------------- 1 | { 2 | "flags":{ 3 | "keep-it-off": { 4 | "clientSide": false, 5 | "deleted": false, 6 | "fallthrough": { 7 | "variation": 0 8 | }, 9 | "key": "keep-it-off", 10 | "offVariation": 1, 11 | "on": false, 12 | "variations": [ 13 | true, 14 | false 15 | ], 16 | "version": 5 17 | }, 18 | "segment-me": { 19 | "clientSide": false, 20 | "deleted": false, 21 | "fallthrough": { 22 | "variation": 0 23 | }, 24 | "key": "segment-me", 25 | "offVariation": 1, 26 | "on": true, 27 | "rules": [ 28 | { 29 | "clauses": [ 30 | { 31 | "attribute": "this-value-does-not-matter", 32 | "negate": false, 33 | "op": "segmentMatch", 34 | "values": [ 35 | "test-included" 36 | ] 37 | } 38 | ], 39 | "id": "ab4a9fb3-7e85-429f-8078-23aa70094540", 40 | "trackEvents": false, 41 | "variation": 1 42 | }, 43 | { 44 | "clauses": [ 45 | { 46 | "attribute": "this-value-does-not-matter", 47 | "negate": true, 48 | "op": "segmentMatch", 49 | "values": [ 50 | "test-included" 51 | ] 52 | } 53 | ], 54 | "id": "489a185d-caaf-4db9-b192-e09e927d070c", 55 | "trackEvents": false, 56 | "variation": 1 57 | } 58 | ], 59 | "variations": [ 60 | true, 61 | false 62 | ], 63 | "version": 5 64 | } 65 | }, 66 | "segments": { 67 | "test-included": { 68 | "deleted": false, 69 | "excluded": [ 70 | "context-33333" 71 | ], 72 | "included": [ 73 | "context-12345", 74 | "context-456" 75 | ], 76 | "key": "test-included", 77 | "version": 8 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /priv/flags-segments-put-data-another1.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "/", 3 | "data": { 4 | "flags": { 5 | "keep-it-off": { 6 | "clientSide": false, 7 | "debugEventsUntilDate": null, 8 | "deleted": false, 9 | "fallthrough": { 10 | "variation": 0 11 | }, 12 | "key": "keep-it-off", 13 | "offVariation": 1, 14 | "on": false, 15 | "prerequisites": [], 16 | "rules": [], 17 | "salt": "d0888ec5921e45c7af5bc10b47b033bb", 18 | "sel": "8b4d79c59adb4df492ebea0bf65dfd4d", 19 | "targets": [], 20 | "trackEvents": true, 21 | "trackEventsFallthrough": false, 22 | "variations": [ 23 | true, 24 | false 25 | ], 26 | "version": 5 27 | }, 28 | "target-me": { 29 | "clientSide": false, 30 | "debugEventsUntilDate": null, 31 | "deleted": false, 32 | "fallthrough": { 33 | "variation": 0 34 | }, 35 | "key": "target-me", 36 | "offVariation": 1, 37 | "on": true, 38 | "prerequisites": [], 39 | "rules": [], 40 | "salt": "YWx0ZXJuYXRlLnBhZ2U=", 41 | "sel": "45501b9314dc4641841af774cb038b96", 42 | "targets": [ 43 | { 44 | "values": [ 45 | "context-12345" 46 | ], 47 | "variation": 0 48 | }, 49 | { 50 | "values": [ 51 | "context-33333" 52 | ], 53 | "variation": 1 54 | } 55 | ], 56 | "trackEvents": true, 57 | "trackEventsFallthrough": false, 58 | "variations": [ 59 | true, 60 | false 61 | ], 62 | "version": 5 63 | }, 64 | "segment-me": { 65 | "clientSide": false, 66 | "debugEventsUntilDate": null, 67 | "deleted": false, 68 | "fallthrough": { 69 | "variation": 0 70 | }, 71 | "key": "segment-me", 72 | "offVariation": 1, 73 | "on": true, 74 | "prerequisites": [], 75 | "rules": [ 76 | { 77 | "clauses": [ 78 | { 79 | "attribute": "this-value-does-not-matter", 80 | "negate": false, 81 | "op": "segmentMatch", 82 | "values": [ 83 | "test-included" 84 | ] 85 | } 86 | ], 87 | "id": "ab4a9fb3-7e85-429f-8078-23aa70094540", 88 | "trackEvents": false, 89 | "variation": 1 90 | }, 91 | { 92 | "clauses": [ 93 | { 94 | "attribute": "this-value-does-not-matter", 95 | "negate": true, 96 | "op": "segmentMatch", 97 | "values": [ 98 | "test-included" 99 | ] 100 | } 101 | ], 102 | "id": "489a185d-caaf-4db9-b192-e09e927d070c", 103 | "trackEvents": false, 104 | "variation": 1 105 | } 106 | ], 107 | "salt": "YWx0ZXJuYXRlLnBhZ2U=", 108 | "sel": "45501b9314dc4641841af774cb038b96", 109 | "targets": [], 110 | "trackEvents": true, 111 | "trackEventsFallthrough": false, 112 | "variations": [ 113 | true, 114 | false 115 | ], 116 | "version": 5 117 | }, 118 | "circular-reference-flag": { 119 | "clientSide": false, 120 | "debugEventsUntilDate": null, 121 | "deleted": false, 122 | "fallthrough": { 123 | "variation": 0 124 | }, 125 | "key": "circular-reference-flag", 126 | "offVariation": 1, 127 | "on": true, 128 | "prerequisites": [{"key":"circular-reference-flag-a", "variation": 0}], 129 | "rules": [], 130 | "salt": "d0888ec5921e45c7af5bc10b47b033bb", 131 | "sel": "8b4d79c59adb4df492ebea0bf65dfd4d", 132 | "targets": [], 133 | "trackEvents": true, 134 | "trackEventsFallthrough": false, 135 | "variations": [ 136 | true, 137 | false 138 | ], 139 | "version": 5 140 | }, 141 | "circular-reference-flag-a": { 142 | "clientSide": false, 143 | "debugEventsUntilDate": null, 144 | "deleted": false, 145 | "fallthrough": { 146 | "variation": 0 147 | }, 148 | "key": "circular-reference-flag-a", 149 | "offVariation": 1, 150 | "on": true, 151 | "prerequisites": [{"key":"circular-reference-flag", "variation": 0}], 152 | "rules": [], 153 | "salt": "d0888ec5921e45c7af5bc10b47b033bb", 154 | "sel": "8b4d79c59adb4df492ebea0bf65dfd4d", 155 | "targets": [], 156 | "trackEvents": true, 157 | "trackEventsFallthrough": false, 158 | "variations": [ 159 | true, 160 | false 161 | ], 162 | "version": 5 163 | } 164 | }, 165 | "segments": { 166 | "test-included": { 167 | "deleted": false, 168 | "excluded": [ 169 | "context-12345", 170 | "context-33333" 171 | ], 172 | "included": [ 173 | "context-45678" 174 | ], 175 | "key": "test-included", 176 | "rules": [], 177 | "salt": "b2ba88c74ad34c288ec10ba78e150afd", 178 | "version": 8 179 | } 180 | } 181 | } 182 | } 183 | 184 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | {deps, [ 3 | {shotgun, "1.1.0"}, 4 | {jsx, "3.1.0"}, 5 | {verl, "1.0.1"}, 6 | {lru, "2.4.0"}, 7 | {uuid, "2.0.2", {pkg, uuid_erl}}, 8 | {eredis, "1.7.1"}, 9 | {yamerl, "0.10.0"}, 10 | {certifi, "~> 2.14"} 11 | ]}. 12 | 13 | {profiles, [ 14 | {test, [ 15 | {deps, [ 16 | {bookish_spork, "0.3.5"}, 17 | {cowboy, "2.8.0"}, 18 | {meck, "0.9.2"} 19 | ]}, 20 | {extra_src_dirs, [{"test", [{recursive, true}]}]} 21 | ]}, 22 | {usage, [ 23 | {extra_src_dirs, [{"test-usage", [{recursive, true}]}]} 24 | ]} 25 | 26 | ]}. 27 | 28 | {shell, [ 29 | {apps, [ldclient]} 30 | ]}. 31 | 32 | {ct_opts, [{ct_hooks, [cth_surefire]}]}. 33 | 34 | {dialyzer, [ 35 | {plt_apps, all_deps} 36 | ]}. 37 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "release-type": "simple", 5 | "versioning": "default", 6 | "bootstrap-sha": "a1f0d642229adc421be6d5eb8fc67c55adec5ff0", 7 | "extra-files": [ 8 | "src/ldclient_config.erl", 9 | "src/ldclient.app.src" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ldclient.app.src: -------------------------------------------------------------------------------- 1 | {application, ldclient, 2 | [{description, "LaunchDarkly SDK for Erlang"}, 3 | {pkg_name, "launchdarkly_server_sdk"}, 4 | {vsn, "3.7.1"}, %% x-release-please-version 5 | {registered, []}, 6 | {mod, {ldclient_app, []}}, 7 | {applications, 8 | [kernel, 9 | stdlib, 10 | inets, 11 | crypto, 12 | asn1, 13 | public_key, 14 | ssl, 15 | shotgun, 16 | jsx, 17 | lru, 18 | verl, 19 | uuid, 20 | eredis, 21 | yamerl 22 | ]}, 23 | {env,[]}, 24 | {modules, []}, 25 | 26 | {licenses, ["Apache 2.0"]}, 27 | {links, [{"GitHub", "https://github.com/launchdarkly/erlang-server-sdk"}]} 28 | ]}. 29 | -------------------------------------------------------------------------------- /src/ldclient_app.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Top level application module 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_app). 8 | 9 | -behaviour(application). 10 | 11 | %% Behavior callbacks 12 | -export([start/2]). 13 | -export([stop/1]). 14 | 15 | %%=================================================================== 16 | %% Behavior callbacks 17 | %%=================================================================== 18 | 19 | start(_Type, _Args) -> 20 | ok = ldclient_config:init(), 21 | ldclient_sup:start_link(). 22 | 23 | stop(_State) -> 24 | ldclient:stop_all_instances(). 25 | -------------------------------------------------------------------------------- /src/ldclient_attribute_reference.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Attribute Reference 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_attribute_reference). 8 | 9 | %% API 10 | -export([ 11 | new/1, 12 | new_from_legacy/1, 13 | escape_legacy/1 14 | ]). 15 | 16 | %% Opaque type, consumers are expected to interact with the type using the methods in this module. 17 | -type attribute_reference() :: #{ 18 | valid := boolean(), 19 | components => [binary()], 20 | %% Binary representation suitable for redaction list. 21 | binary => binary() 22 | }. 23 | 24 | -export_type([attribute_reference/0]). 25 | 26 | %%=================================================================== 27 | %% Internal functions 28 | %%=================================================================== 29 | 30 | -type unescape_state() :: #{ 31 | valid := boolean(), 32 | acc := binary(), 33 | in_escape := boolean() 34 | }. 35 | 36 | %% Expose non-exported methods for tests. 37 | -ifdef(TEST). 38 | -compile(export_all). 39 | -endif. 40 | 41 | %% @doc Create a new attribute reference from an attribute reference string. 42 | %% If you want to create a reference from a legacy attribute name, then use new_from_legacy. 43 | %% @end 44 | -spec new(BinaryReference :: binary()) -> attribute_reference(). 45 | new(<<"">> = _BinaryReference) -> 46 | #{valid => false}; 47 | new(<<"/">> = _BinaryReference) -> 48 | #{valid => false}; 49 | new(<<"/", _T/binary>> = BinaryReference) -> 50 | Components = get_components(BinaryReference), 51 | #{valid => valid_components(Components), binary => BinaryReference, components => Components}; 52 | %% Binary did not start with "/", so it is a literal. 53 | new(BinaryReference) when is_binary(BinaryReference) -> 54 | #{valid => true, components => [BinaryReference], binary => BinaryReference}; 55 | %% Was not a binary value, so it is invalid. 56 | new(_) -> 57 | #{valid => false}. 58 | 59 | %% @doc Create a new attribute reference from a legacy attribute name. 60 | %% 61 | %% @end 62 | -spec new_from_legacy(LegacyLiteral :: binary()) -> attribute_reference(). 63 | %% An empty string is an invalid literal. 64 | new_from_legacy(<<"">> = _LegacyLiteral) -> #{valid => false}; 65 | %% If a legacy literal starts with /, then it will need escaped for the binary reference. 66 | new_from_legacy(<<"/", _T/binary>> = LegacyLiteral) -> new(escape_legacy(LegacyLiteral)); 67 | new_from_legacy(LegacyLiteral) -> new(LegacyLiteral). 68 | 69 | %% @doc Escape an attribute name to an attribute reference. 70 | %% 71 | %% @end 72 | -spec escape_legacy(Component :: binary()) -> EscapedComponent :: binary(). 73 | escape_legacy(Component) -> escape_legacy(Component, <<"/">>). 74 | 75 | %%=================================================================== 76 | %% Internal functions 77 | %%=================================================================== 78 | 79 | -spec unescape(Component :: binary()) -> UnescapedComponent :: binary() | error. 80 | unescape(Component) -> 81 | unescape(Component, #{acc => <<"">>, valid => true, in_escape => false}). 82 | 83 | -spec unescape(Component :: binary(), State :: unescape_state()) -> UnescapedComponent :: binary() | error. 84 | unescape(<<"~">>, _State) -> 85 | error; 86 | unescape(<<"~", T/binary>>, State) -> 87 | unescape(T, State#{ in_escape => true }); 88 | unescape(<<"0", T/binary>>, #{in_escape := true, acc := Acc} = State) -> 89 | unescape(T, State#{ acc => <>, in_escape => false}); 90 | unescape(<<"1", T/binary>>, #{in_escape := true, acc := Acc} = State) -> 91 | unescape(T, State#{ acc => <>, in_escape => false}); 92 | unescape(<<_H, _T/binary>>, #{in_escape := true} = _State) -> 93 | %% Was in an escape sequence and the next character was not 0 or 1. 94 | error; 95 | unescape(<>, #{acc := Acc} = State) -> 96 | unescape(T, State#{acc => <>}); 97 | unescape(<<>>, #{acc := Acc } = _State) -> Acc. 98 | 99 | -spec get_components(BinaryReference :: binary()) -> Components :: [binary() | error]. 100 | get_components(BinaryReference) -> 101 | %% Skip the first item in the split because of the leading /. 102 | lists:map(fun unescape/1, tl(binary:split(BinaryReference, <<"/">>, [global]))). 103 | 104 | -spec valid_components([error | binary()]) -> boolean(). 105 | valid_components([error|_T]) -> false; 106 | %% Empty components indicate multiple or trailing /. 107 | %% /potato/ or /potato//potato 108 | valid_components([<<>>|_T]) -> false; 109 | valid_components([_H|T]) -> valid_components(T); 110 | valid_components([]) -> true. 111 | 112 | -spec escape_legacy(Component :: binary(), Acc :: binary()) -> Escaped :: binary(). 113 | escape_legacy(<<"~", T/binary>> = _Component, Acc) -> escape_legacy(T, <>); 114 | escape_legacy(<<"/", T/binary>> = _Component, Acc) -> escape_legacy(T, <>); 115 | escape_legacy(<> = _Component, Acc) -> escape_legacy(T, <>); 116 | escape_legacy(<<>> = _Component, Acc) -> Acc. 117 | -------------------------------------------------------------------------------- /src/ldclient_backoff.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_backoff). 8 | 9 | %% API 10 | -export([init/4, init/5, fail/1, succeed/1, fire/1]). 11 | 12 | %% Types 13 | 14 | -type backoff() :: #{ 15 | initial => non_neg_integer(), 16 | current => non_neg_integer(), 17 | max => non_neg_integer(), 18 | attempt => non_neg_integer(), 19 | active_since => integer() | undefined, 20 | destination => pid(), 21 | value => term(), 22 | max_exp => float(), 23 | uniform => fun(() -> float()) 24 | }. 25 | 26 | -define(JITTER_RATIO, 0.5). 27 | 28 | %% Reset interval in seconds. 29 | -define(RESET_INTERVAL, 60). 30 | 31 | -export_type([backoff/0]). 32 | 33 | %% Expose non-exported methods for tests. 34 | -ifdef(TEST). 35 | -compile(export_all). 36 | -endif. 37 | 38 | -spec init(Initial :: non_neg_integer(), Max :: non_neg_integer(), Destination :: pid(), Value :: term()) -> backoff(). 39 | init(Initial, Max, Destination, Value) -> 40 | init(Initial, Max, Destination, Value, fun() -> rand:uniform() end). 41 | 42 | %% This version of the function exists for testing and allows injecting the random number source. 43 | -spec init(Initial :: non_neg_integer(), Max :: non_neg_integer(), Destination :: pid(), Value :: term(), Uniform :: fun(() -> float())) -> backoff(). 44 | init(Initial, Max, Destination, Value, Uniform) -> 45 | SafeInitial = lists:max([Initial, 1]), 46 | #{ 47 | initial => SafeInitial, %% Do not allow initial delay to be 0 or negative. 48 | current => SafeInitial, 49 | max => Max, 50 | attempt => 0, 51 | active_since => undefined, 52 | destination => Destination, 53 | value => Value, 54 | %% The exponent at which the backoff delay will exceed the maximum. 55 | %% Beyond this limit the backoff can be set to the max. 56 | max_exp => math:ceil(math:log2(Max/SafeInitial)), 57 | %% For reasonable values this should ensure we never overflow. 58 | %% Note that while integers can be arbitrarily large the math library uses C functions 59 | %% that are implemented with floats. 60 | %% Allow for alternate random number source. 61 | uniform => Uniform 62 | }. 63 | 64 | %% @doc Get an updated backoff with updated delay. Does not start a timer automatically. 65 | %% Use fire/1 to start a timer with the updated backoff. 66 | %% @end 67 | -spec fail(Backoff :: backoff()) -> backoff(). 68 | fail(#{active_since := undefined} = Backoff) -> 69 | %% ActiveSince is undefined, so we have not had a successful connection since the last attempt. 70 | update_backoff(Backoff, undefined); 71 | fail(#{active_since := ActiveSince} = Backoff) -> 72 | ActiveDuration = ldclient_time:time_seconds() - ActiveSince, 73 | update_backoff(Backoff, ActiveDuration). 74 | 75 | %% @doc Get an updated backoff with information about the successful connection attempt. 76 | %% Does not clear any timers. Use the timer handle from fire/1 to cancel pending timers. 77 | %% @end 78 | -spec succeed(Backoff :: backoff()) -> backoff(). 79 | succeed(Backoff) -> 80 | Backoff#{active_since => ldclient_time:time_seconds()}. 81 | 82 | %% @doc Start a timer with the current delay. 83 | %% @end 84 | -spec fire(backoff()) -> Timer :: reference(). 85 | fire(#{current := Current, destination := Destination, value := Value}) -> 86 | ldclient_time:start_timer(Current, Destination, Value). 87 | 88 | %%=================================================================== 89 | %% Internal functions 90 | %%=================================================================== 91 | 92 | -spec update_backoff(Backoff :: backoff(), ActiveSince :: integer() | undefined) -> backoff(). 93 | update_backoff(#{attempt := Attempt} = Backoff, undefined = _ActiveDuration) -> 94 | %% There has not been a successful connection. 95 | NewAttempt = Attempt + 1, 96 | Backoff#{current => delay(NewAttempt, Backoff), attempt => NewAttempt}; 97 | update_backoff(Backoff, ActiveDuration) when ActiveDuration > ?RESET_INTERVAL -> 98 | %% The last successful connection was active for more than 1 minute. 99 | NewAttempt = 1, 100 | Backoff#{current => delay(NewAttempt, Backoff), attempt => NewAttempt, active_since => undefined}; 101 | update_backoff(#{attempt := Attempt} = Backoff, _ActiveDuration) -> 102 | %% The last successful connection was less than 1 minute, 103 | NewAttempt = Attempt + 1, 104 | %% Resetting ActiveSince here, otherwise after being disconnected for a minute it would 105 | %% reset the the AttemptCount. 106 | Backoff#{current => delay(NewAttempt, Backoff), attempt => NewAttempt, active_since => undefined}. 107 | 108 | -spec delay(Attempt :: non_neg_integer(), Backoff :: backoff()) -> non_neg_integer(). 109 | delay(Attempt, #{initial := Initial, max := Max, max_exp := MaxExp, uniform := Uniform} = _Backoff) 110 | when Attempt - 1 < MaxExp -> 111 | jitter(min(backoff(Initial, Attempt), Max), Uniform); 112 | delay(_Attempt, #{max := Max, uniform := Uniform} = _Backoff) -> 113 | jitter(Max, Uniform). 114 | 115 | -spec backoff(Initial :: non_neg_integer(), Attempt :: non_neg_integer()) -> non_neg_integer(). 116 | backoff(Initial, Attempt) -> 117 | trunc(Initial * (math:pow(2, Attempt - 1))). 118 | 119 | -spec jitter(Value :: non_neg_integer(), Uniform :: fun(() -> float())) -> non_neg_integer(). 120 | jitter(Value, Uniform) -> 121 | trunc(Value - (Uniform() * ?JITTER_RATIO * Value)). 122 | -------------------------------------------------------------------------------- /src/ldclient_context_cache.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc User LRU cache 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_context_cache). 8 | 9 | %% API 10 | -export([get_local_reg_name/1]). 11 | -export([notice_context/2]). 12 | 13 | %%=================================================================== 14 | %% API 15 | %%=================================================================== 16 | 17 | -spec get_local_reg_name(Tag :: atom()) -> atom(). 18 | get_local_reg_name(Tag) -> 19 | list_to_atom("ldclient_context_cache_" ++ atom_to_list(Tag)). 20 | 21 | %% @doc Add to the set of the users we've noticed, and return true if the user 22 | %% was already known to us. 23 | %% 24 | %% @end 25 | -spec notice_context(Tag :: atom(), Context :: ldclient_context:context()) -> boolean(). 26 | notice_context(Tag, Context) -> 27 | CanonicalKey = ldclient_context:get_canonical_key(Context), 28 | notice_context_canonical_key(Tag, CanonicalKey). 29 | 30 | -spec notice_context_canonical_key(Tag :: atom(), CanonicalKey :: binary()) -> boolean(). 31 | notice_context_canonical_key(_Tag, <<>>) -> 32 | %% Do not add to the cache. Returning true also means we should not send an index for this invalid user. 33 | true; 34 | notice_context_canonical_key(Tag, CanonicalKey) -> 35 | CacheServer = get_local_reg_name(Tag), 36 | {Exists, _} = lru:contains_or_add(whereis(CacheServer), CanonicalKey, CanonicalKey), 37 | Exists. 38 | -------------------------------------------------------------------------------- /src/ldclient_eval_reason.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Reason 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | -module(ldclient_eval_reason). 7 | 8 | %% API 9 | -export([format/1]). 10 | 11 | -spec format(ldclient_eval:reason()) -> map(). 12 | format(target_match) -> #{<<"kind">> => <<"TARGET_MATCH">>}; 13 | format({rule_match, RuleIndex, RuleId}) -> #{kind => <<"RULE_MATCH">>, ruleIndex => RuleIndex, ruleId => RuleId}; 14 | format({rule_match, RuleIndex, RuleId, in_experiment}) -> #{kind => <<"RULE_MATCH">>, ruleIndex => RuleIndex, ruleId => RuleId, inExperiment => true}; 15 | format({prerequisite_failed, [PrereqKey|_]}) -> #{kind => <<"PREREQUISITE_FAILED">>, prerequisiteKey => PrereqKey}; 16 | format({error, client_not_ready}) -> #{kind => <<"ERROR">>, errorKind => <<"CLIENT_NOT_READY">>}; 17 | format({error, flag_not_found}) -> #{kind => <<"ERROR">>, errorKind => <<"FLAG_NOT_FOUND">>}; 18 | format({error, malformed_flag}) -> #{kind => <<"ERROR">>, errorKind => <<"MALFORMED_FLAG">>}; 19 | format({error, user_not_specified}) -> #{kind => <<"ERROR">>, errorKind => <<"USER_NOT_SPECIFIED">>}; 20 | format({error, wrong_type}) -> #{kind => <<"ERROR">>, errorKind => <<"WRONG_TYPE">>}; 21 | format({error, exception}) -> #{kind => <<"ERROR">>, errorKind => <<"EXCEPTION">>}; 22 | format(fallthrough) -> #{kind => <<"FALLTHROUGH">>}; 23 | format({fallthrough, in_experiment}) -> #{kind => <<"FALLTHROUGH">>, inExperiment => true}; 24 | format(off) -> #{kind => <<"OFF">>}. 25 | -------------------------------------------------------------------------------- /src/ldclient_event_dispatch.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_event_dispatch' module 3 | %% @private 4 | %% This is a behavior that event dispatchers must implement. It is used to send 5 | %% event batches to LaunchDarkly. 6 | %% @end 7 | %%------------------------------------------------------------------- 8 | 9 | -module(ldclient_event_dispatch). 10 | 11 | %% `send' must dispatch the batch of events. It takes the list of events, the 12 | %% destination URI and SDK key. It must return success or temporary or 13 | %% permanent failure. 14 | -callback send(State:: any(), OutputEvents :: binary(), PayloadId :: uuid:uuid(), Uri :: string()) -> 15 | {ok, integer()} | {error, temporary, string()} | {error, permanent, string()}. 16 | 17 | %% `init' should return an initial value for the `State' argument to `send' 18 | -callback init(Tag :: atom(), SdkKey :: string()) -> any(). 19 | -------------------------------------------------------------------------------- /src/ldclient_event_dispatch_httpc.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Event dispatcher 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_event_dispatch_httpc). 8 | 9 | -behaviour(ldclient_event_dispatch). 10 | 11 | %% Behavior callbacks 12 | -export([init/2, send/4]). 13 | 14 | %% Internal type for ETag cache state 15 | -type state() :: #{ 16 | headers => list(), 17 | http_options => list() 18 | }. 19 | 20 | %% Expose non-exported methods for tests. 21 | -ifdef(TEST). 22 | -compile(export_all). 23 | -endif. 24 | 25 | %%=================================================================== 26 | %% Behavior callbacks 27 | %%=================================================================== 28 | 29 | -spec init(Tag :: atom(), SdkKey :: string()) -> state(). 30 | init(Tag, _SdkKey) -> 31 | Options = ldclient_config:get_value(Tag, http_options), 32 | HttpOptions = ldclient_http_options:httpc_parse_http_options(Options), 33 | DefaultHeaders = ldclient_headers:get_default_headers(Tag, string_pairs), 34 | Headers = ldclient_http_options:httpc_append_custom_headers([ 35 | {"X-LaunchDarkly-Event-Schema", ldclient_config:get_event_schema()} 36 | | DefaultHeaders 37 | ], Options), 38 | #{ 39 | headers => Headers, 40 | http_options => HttpOptions 41 | }. 42 | 43 | %% @doc Send events to LaunchDarkly event server 44 | %% 45 | %% @end 46 | -spec send(State :: state(), JsonEvents :: binary(), PayloadId :: uuid:uuid(), Uri :: string()) -> 47 | {ok, integer()} | {error, temporary, string()} | {error, permanent, string()}. 48 | send(State, JsonEvents, PayloadId, Uri) -> 49 | #{headers := BaseHeaders, http_options := HttpOptions} = State, 50 | Headers = [ 51 | {"X-LaunchDarkly-Payload-ID", uuid:uuid_to_string(PayloadId)} | 52 | BaseHeaders 53 | ], 54 | Request = httpc:request(post, {Uri, Headers, "application/json", JsonEvents}, HttpOptions, []), 55 | process_request(Request). 56 | 57 | %%=================================================================== 58 | %% Internal functions 59 | %%=================================================================== 60 | 61 | -type http_request() :: {ok, {{string(), integer(), string()}, [{string(), string()}], string() | binary()}}. 62 | 63 | -spec process_request({error, term()} | http_request()) 64 | -> {ok, integer()} | {error, temporary, string()} | {error, permanent, string()}. 65 | process_request({error, Reason}) -> 66 | {error, temporary, Reason}; 67 | process_request({ok, {{_Version, StatusCode, _ReasonPhrase}, Headers, _Body}}) when StatusCode < 400 -> 68 | {ok, get_server_time(Headers)}; 69 | process_request({ok, {{Version, StatusCode, ReasonPhrase}, _Headers, _Body}}) -> 70 | Reason = format_response(Version, StatusCode, ReasonPhrase), 71 | HttpErrorType = ldclient_http:is_http_error_code_recoverable(StatusCode), 72 | {error, HttpErrorType, Reason}. 73 | 74 | -spec format_response(Version :: string(), StatusCode :: integer(), ReasonPhrase :: string()) -> 75 | string(). 76 | format_response(Version, StatusCode, ReasonPhrase) -> 77 | io_lib:format("~s ~b ~s", [Version, StatusCode, ReasonPhrase]). 78 | 79 | %% Get the server time, and if there is not time, then return 0. 80 | -spec get_server_time(Headers :: [{Field :: [byte()], Value :: binary() | iolist()}]) -> integer(). 81 | get_server_time([{"date", Date}|_T]) when is_list(Date) -> 82 | %% convert_request_date expects a string that is a list of characters. 83 | %% Not a binary string. The guard can make sure it is a list, but not 84 | %% that it is a char list. So that gets checked here. 85 | case io_lib:char_list(Date) of 86 | true -> case httpd_util:convert_request_date(Date) of 87 | bad_date -> 88 | %% This would be a date in a bad format. 89 | 0; 90 | ParsedDate -> 91 | ldclient_time:datetime_to_timestamp(ParsedDate) 92 | end; 93 | false -> 94 | %% The date was a list, but was not a list of characters. 95 | 0 96 | end; 97 | get_server_time([_H|T]) -> 98 | get_server_time(T); 99 | get_server_time(_) -> 0. 100 | -------------------------------------------------------------------------------- /src/ldclient_event_sup.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Event supervisor 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_event_sup). 8 | 9 | -behaviour(supervisor). 10 | 11 | %% Supervision 12 | -export([start_link/2, init/1]). 13 | 14 | %% Helper macro for declaring children of supervisor 15 | -define(CHILD(Id, Module, Args, Type), {Id, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 16 | 17 | %%=================================================================== 18 | %% Supervision 19 | %%=================================================================== 20 | 21 | -spec start_link(SupName :: atom(), Tag :: atom()) -> 22 | {ok, Pid :: pid()} | ignore | {error, Reason :: term()}. 23 | start_link(SupName, Tag) -> 24 | error_logger:info_msg("Starting event supervisor for ~p with name ~p", [Tag, SupName]), 25 | supervisor:start_link({local, SupName}, ?MODULE, [Tag]). 26 | 27 | -spec init(Args :: term()) -> 28 | {ok, {{supervisor:strategy(), non_neg_integer(), pos_integer()}, [supervisor:child_spec()]}}. 29 | init([Tag]) -> 30 | {ok, {{one_for_one, 1, 5}, children(Tag)}}. 31 | 32 | %%=================================================================== 33 | %% Internal functions 34 | %%=================================================================== 35 | 36 | -spec children(Tag :: atom()) -> [supervisor:child_spec()]. 37 | children(Tag) -> 38 | UserCacheName = ldclient_context_cache:get_local_reg_name(Tag), 39 | ContextKeysCapacity = ldclient_config:get_value(Tag, context_keys_capacity), 40 | UserCacheWorker = ?CHILD(lru, lru, [{local, UserCacheName}, [{max_objs, ContextKeysCapacity}]], worker), 41 | EventStorageWorker = ?CHILD(ldclient_event_server, ldclient_event_server, [Tag], worker), 42 | EventProcessWorker = ?CHILD(ldclient_event_process_server, ldclient_event_process_server, [Tag], worker), 43 | [UserCacheWorker, EventStorageWorker, EventProcessWorker]. 44 | -------------------------------------------------------------------------------- /src/ldclient_headers.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Methods for dealing with http headers. 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | -module(ldclient_headers). 7 | 8 | %% API 9 | -export([get_default_headers/2]). 10 | 11 | -ifdef(TEST). 12 | -compile(export_all). 13 | -endif. 14 | 15 | %% @doc Get the default headers for SDK requests. 16 | %% 17 | %% The headers can be accessed as a map of binary values, or as a list of pairs containing string() values. 18 | %% This option is offered because different client libraries used have different header input formats. 19 | %% ``` 20 | %% %% binary_map 21 | %% #{<<"some-header">> => <<"some-value">>} 22 | %% %% string_pairs 23 | %% [{"some-header", "some-value"}] 24 | %% ''' 25 | %% @end 26 | -spec get_default_headers(Tag :: atom(), Format :: binary_map | string_pairs) -> map() | [{string(), string()}]. 27 | get_default_headers(Tag, _Format = binary_map) -> 28 | get_default_headers(Tag); 29 | get_default_headers(Tag, _Format = string_pairs) -> 30 | %% Translate the headers from a map into a list of {binary(), binary()}. 31 | PairList = maps:to_list(get_default_headers(Tag)), 32 | %% Translate the {binary(), binary()} list into a {string(), string()} list. 33 | lists:map(fun({Key, Value}) -> {binary:bin_to_list(Key), binary:bin_to_list(Value)} end, PairList). 34 | 35 | %%=================================================================== 36 | %% Internal functions 37 | %%=================================================================== 38 | 39 | %% @doc Get the default headers, as a map, for the specified client Tag. 40 | %% 41 | %% @end 42 | get_default_headers(Tag) -> 43 | with_tags(Tag, #{ 44 | <<"authorization">> => list_to_binary(ldclient_config:get_value(Tag, sdk_key)), 45 | <<"user-agent">> => list_to_binary(ldclient_config:get_user_agent()) 46 | }). 47 | 48 | %% @doc Append the tags header to the given map. 49 | %% 50 | %% LaunchDarkly supports adding `tag' headers to requests. These are those tags. 51 | %% Terminology wise this overlaps with the `Tag' used to identify client instances. 52 | %% @end 53 | -spec with_tags(Tag :: atom(), InMap :: map()) -> OutMap :: map(). 54 | with_tags(Tag, InMap) -> 55 | Tags = get_tags(Tag), 56 | case Tags of 57 | [] -> InMap; 58 | _ -> InMap#{ 59 | <<"x-launchdarkly-tags">> => combine_tags(Tags) 60 | } 61 | end. 62 | 63 | %% @doc Get the tags as a list of binary pairs. 64 | %% 65 | %% @end 66 | -spec get_tags(Tag :: atom()) -> [{binary(), [binary()]}]. 67 | get_tags(Tag) -> 68 | %% This is where additional tags should be added. 69 | AppInfo = ldclient_config:get_value(Tag, application), 70 | sort_tags(add_version_tag(AppInfo, add_id_tag(AppInfo, []))). 71 | 72 | %% @doc Combine all the tags into the format for application tags. 73 | %% 74 | %% Formatted tags are of the format `tag/value' a tag can have multiple values 75 | %% in that case it will be represented by space delimited pairs `tagA/valueA tagA/ValueB'. 76 | %% @end 77 | -spec combine_tags(Tags :: [{binary(), [binary()]}]) -> binary(). 78 | combine_tags(Tags) -> combine_tags(Tags, <<>>). 79 | 80 | -spec combine_tags(Tags :: [{binary(), [binary()]}], AccIn :: binary()) -> binary(). 81 | combine_tags([], AccIn) -> AccIn; 82 | combine_tags([Tag|Remainder] = _Tags, <<>>) -> 83 | combine_tags(Remainder, combine_tag(Tag)); 84 | combine_tags([Tag|Remainder] = _Tags, AccIn) -> 85 | join(<<" ">>, [AccIn, combine_tags(Remainder, combine_tag(Tag))]). 86 | 87 | -spec combine_tag(Tag :: {binary(), [binary()]}) -> binary(). 88 | combine_tag({TagKey, TagValues}) -> 89 | join(<<" ">>, lists:map(fun(TagValue) -> <> end, TagValues)). 90 | 91 | %% @doc Join a list of binary() into a single binary() with a separator. 92 | %% 93 | %% @end 94 | -spec join(Separator :: binary(), List :: [binary()]) -> binary(). 95 | join(_Separator, []) -> 96 | <<>>; 97 | join(Separator, [H|T]) -> 98 | lists:foldl(fun (Value, Acc) -> <> end, H, T). 99 | 100 | add_id_tag(#{id := Id} = _Info, InList) -> [{<<"application-id">>, [Id]}| InList]; 101 | add_id_tag(_Info, InList) -> InList. 102 | 103 | add_version_tag(#{version := Version} = _Info, InList) -> [{<<"application-version">>, [Version]}| InList]; 104 | add_version_tag(_Info, InList) -> InList. 105 | 106 | %% @doc Tags and values for tags should both be sorted. 107 | %% 108 | %% ``` 109 | %% [ 110 | %% {<<"b">>, [<<"2">>, <<"1">>, <<"3">>]}, 111 | %% {<<"c">>, [<<"5">>, <<"4">>, <<"1">>]}, 112 | %% {<<"a">>, [<<"7">>, <<"9">>, <<"3">>]} 113 | %% ] 114 | %% %% Would sort to: 115 | %% [ 116 | %% {<<"a">>, [<<"3">>, <<"7">>, <<"9">>]}, 117 | %% {<<"b">>, [<<"1">>, <<"2">>, <<"3">>]}, 118 | %% {<<"c">>, [<<"1">>, <<"4">>, <<"5">>]} 119 | %% ] 120 | %% ''' 121 | %% @end 122 | -spec sort_tags(Tags :: [{binary(), [binary()]}]) -> SortedTags :: [{binary(), [binary()]}]. 123 | sort_tags(Tags) -> 124 | %% Sort the values of each tag. 125 | lists:map(fun({Key, Value}) -> {Key, sort_tag_values(Value)} end, 126 | %% Sort the tags by key. 127 | lists:sort(fun({KeyA, _ValueA} = _A, {KeyB, _ValueB} = _B) -> KeyA < KeyB end, Tags)). 128 | 129 | -spec sort_tag_values(TagValue :: [binary()]) -> [binary()]. 130 | sort_tag_values(TagValue) -> lists:sort(TagValue). 131 | -------------------------------------------------------------------------------- /src/ldclient_http.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc HTTP utilities 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | -module(ldclient_http). 7 | 8 | %% API 9 | -export([ 10 | uri_parse/1, 11 | is_http_error_code_recoverable/1 12 | ]). 13 | 14 | -spec port_for_scheme(Port :: undefined | inet:port_number(), Scheme :: atom()) -> inet:port_number(). 15 | port_for_scheme(undefined, undefined) -> 16 | 80; 17 | port_for_scheme(undefined, Scheme) -> 18 | case Scheme of 19 | https -> 443; 20 | _ -> 80 21 | end; 22 | port_for_scheme(Port, _Scheme) -> 23 | Port. 24 | 25 | -type uri_string() :: string() | binary(). 26 | -type parse_result() :: {atom(), uri_string(), inet:port_number(), uri_string(), uri_string()}. 27 | 28 | -spec uri_parse(URL :: uri_string()) -> {ok, parse_result()}. 29 | uri_parse(URL) -> 30 | Defaults = #{query => "", path => "", scheme => http, host => "", port => undefined}, 31 | Parsed = uri_string:parse(URL), 32 | Merged = maps:merge(Defaults, Parsed), 33 | #{scheme:=Scheme,host:=Host,port:=Port,path:=Path,query:=Query} = Merged, 34 | SchemeAtom = list_to_atom(Scheme), 35 | {ok, {SchemeAtom, Host, port_for_scheme(Port, SchemeAtom), Path, Query}}. 36 | 37 | -spec is_http_error_code_recoverable(StatusCode :: integer()) -> temporary | permanent. 38 | is_http_error_code_recoverable(400) -> temporary; 39 | is_http_error_code_recoverable(408) -> temporary; 40 | is_http_error_code_recoverable(429) -> temporary; 41 | is_http_error_code_recoverable(StatusCode) when StatusCode >= 500 -> temporary; 42 | is_http_error_code_recoverable(_) -> permanent. 43 | -------------------------------------------------------------------------------- /src/ldclient_http_options.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Http option parsing for http and gun. 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_http_options). 8 | 9 | %% API 10 | -export([httpc_parse_http_options/1]). 11 | -export([httpc_parse_http_options/2]). 12 | -export([httpc_append_custom_headers/2]). 13 | 14 | -export([gun_parse_http_options/1]). 15 | -export([gun_parse_http_options/2]). 16 | -export([gun_append_custom_headers/2]). 17 | 18 | %% Implementation for httpc. 19 | 20 | -spec httpc_parse_http_options(Options :: ldclient_config:http_options(), BaseOptions :: list()) -> list(). 21 | httpc_parse_http_options(Options, BaseOptions) -> 22 | OptionsWithTls = httpc_parse_tls_options(BaseOptions, Options), 23 | httpc_parse_connect_timeout(OptionsWithTls, Options). 24 | 25 | -spec httpc_parse_http_options(Options :: ldclient_config:http_options()) -> list(). 26 | httpc_parse_http_options(Options) -> 27 | httpc_parse_http_options(Options, []). 28 | 29 | -spec httpc_parse_tls_options(HttpOptions :: list(), ConfigOptions :: ldclient_config:http_options()) -> list(). 30 | httpc_parse_tls_options(HttpOptions, #{tls_options := undefined}) -> HttpOptions; 31 | httpc_parse_tls_options(HttpOptions, #{tls_options := TlsOptions}) -> 32 | [{ssl, TlsOptions} | HttpOptions]. 33 | 34 | -spec httpc_parse_connect_timeout(HttpOptions :: list(), ConfigOptions :: ldclient_config:http_options()) -> list(). 35 | httpc_parse_connect_timeout(HttpOptions, #{connect_timeout := undefined}) -> HttpOptions; 36 | httpc_parse_connect_timeout(HttpOptions, #{connect_timeout := TimeoutTime}) -> 37 | [{connect_timeout, TimeoutTime} | HttpOptions]. 38 | 39 | -spec httpc_append_custom_headers(Headers :: list(), ConfigOptions :: ldclient_config:http_options()) -> list(). 40 | httpc_append_custom_headers(Headers, #{custom_headers := undefined}) -> Headers; 41 | httpc_append_custom_headers(Headers, #{custom_headers := CustomHeaders}) -> lists:append(Headers, CustomHeaders). 42 | 43 | %% Implementation for gun 44 | 45 | -spec gun_parse_http_options(Options :: ldclient_config:http_options()) -> map(). 46 | gun_parse_http_options(undefined) -> 47 | #{retry => 0, protocols => [http]}; 48 | gun_parse_http_options(HttpOptions) -> 49 | gun_parse_http_options(HttpOptions, #{ 50 | retry => 0, protocols => [http] 51 | }). 52 | 53 | -spec gun_parse_http_options(Options :: ldclient_config:http_options(), GunOptions :: map()) -> map(). 54 | gun_parse_http_options(HttpOptions, GunOptions) -> 55 | OptionsWithTls = gun_parse_tls_options(HttpOptions, GunOptions), 56 | gun_parse_connect_timeout(HttpOptions, OptionsWithTls). 57 | 58 | -spec gun_parse_tls_options(ConfigOptions :: ldclient_config:http_options(), GunOptions :: map()) -> map(). 59 | gun_parse_tls_options(#{tls_options := undefined}, GunOptions) -> GunOptions; 60 | gun_parse_tls_options(#{tls_options := TlsOptions}, GunOptions) -> 61 | GunOptions#{ 62 | tls_opts => TlsOptions 63 | }. 64 | 65 | -spec gun_append_custom_headers(Headers :: map(), ConfigOptions :: ldclient_config:http_options()) -> map(). 66 | gun_append_custom_headers(Headers, #{custom_headers := undefined}) -> Headers; 67 | gun_append_custom_headers(Headers, #{custom_headers := CustomHeaders}) -> 68 | maps:merge(Headers, maps:from_list(CustomHeaders)). 69 | 70 | -spec gun_parse_connect_timeout(ConfigOptions :: ldclient_config:http_options(), GunOptions :: map()) -> map(). 71 | gun_parse_connect_timeout(#{connect_timeout := undefined}, GunOptions) -> GunOptions; 72 | gun_parse_connect_timeout(#{connect_timeout := TimeoutTime}, GunOptions) -> 73 | GunOptions#{ 74 | connect_timeout => TimeoutTime 75 | }. 76 | -------------------------------------------------------------------------------- /src/ldclient_instance_sup.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Instance supervisor 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_instance_sup). 8 | 9 | -behaviour(supervisor). 10 | 11 | %% Supervision 12 | -export([start_link/5, init/1, child_spec/1, child_spec/2]). 13 | 14 | %% Helper macro for declaring children of supervisor 15 | -define(CHILD(Id, Module, Args, Type), {Id, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 16 | 17 | child_spec(Args) -> child_spec(?MODULE, Args). 18 | child_spec(Id, Args) -> 19 | #{ 20 | id => Id, 21 | start => {?MODULE, start_link, Args}, 22 | restart => permanent, 23 | shutdown => 5000, % shutdown time 24 | type => supervisor, 25 | modules => [?MODULE] 26 | }. 27 | %%=================================================================== 28 | %% Supervision 29 | %%=================================================================== 30 | 31 | -spec start_link( 32 | SupName :: atom(), 33 | UpdateSupName :: atom(), 34 | UpdateWorkerModule :: atom(), 35 | EventSupName :: atom(), 36 | Tag :: atom() 37 | ) -> 38 | {ok, Pid :: pid()} | ignore | {error, Reason :: term()}. 39 | start_link(SupName, UpdateSupName, UpdateWorkerModule, EventSupName, Tag) -> 40 | error_logger:info_msg("Starting instance supervisor for ~p with name ~p", [Tag, SupName]), 41 | supervisor:start_link({local, SupName}, ?MODULE, [UpdateSupName, UpdateWorkerModule, EventSupName, Tag]). 42 | 43 | -spec init(Args :: term()) -> 44 | {ok, {{supervisor:strategy(), non_neg_integer(), pos_integer()}, [supervisor:child_spec()]}}. 45 | init([UpdateSupName, UpdateWorkerModule, EventSupName, Tag]) -> 46 | {ok, {{one_for_one, 1, 5}, children(UpdateSupName, UpdateWorkerModule, EventSupName, Tag)}}. 47 | 48 | %%=================================================================== 49 | %% Internal functions 50 | %%=================================================================== 51 | 52 | -spec children( 53 | UpdateSupName :: atom(), 54 | UpdateWorkerModule :: atom(), 55 | EventSupName :: atom(), 56 | Tag :: atom() 57 | ) -> [supervisor:child_spec()]. 58 | children(UpdateSupName, UpdateWorkerModule, EventSupName, Tag) -> 59 | UpdateSup = ?CHILD(ldclient_update_sup, ldclient_update_sup, [UpdateSupName, UpdateWorkerModule], supervisor), 60 | EventSup = ?CHILD(ldclient_event_sup, ldclient_event_sup, [EventSupName, Tag], supervisor), 61 | [UpdateSup, EventSup]. 62 | -------------------------------------------------------------------------------- /src/ldclient_rule.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Flag rule data type 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_rule). 8 | 9 | %% API 10 | -export([new/1]). 11 | -export([match_context/4]). 12 | 13 | %% Types 14 | -type rule() :: #{ 15 | id => binary(), 16 | clauses => [ldclient_clause:clause()], 17 | trackEvents => boolean(), 18 | variationOrRollout => ldclient_flag:variation_or_rollout() 19 | }. 20 | %% Expresses a set of AND-ed matching conditions for a context, along with either 21 | %% a fixed variation or a set of rollout percentages 22 | 23 | -export_type([rule/0]). 24 | 25 | %%=================================================================== 26 | %% API 27 | %%=================================================================== 28 | 29 | -spec new(map()) -> rule(). 30 | new(RawRuleMap) -> 31 | RuleTemplate = #{ 32 | <<"id">> => <<>>, 33 | <<"clauses">> => [], 34 | <<"trackEvents">> => false 35 | }, 36 | RuleMap = maps:merge(RuleTemplate, RawRuleMap), 37 | new_from_template(RuleMap). 38 | 39 | %% @doc Match all clauses to context, includes segmentMatch 40 | %% 41 | %% @end 42 | -spec match_context(rule(), Context :: ldclient_context:context(), FeatureStore :: atom(), Tag :: atom()) -> match | no_match | malformed_flag. 43 | match_context(#{clauses := Clauses}, Context, FeatureStore, Tag) -> 44 | check_clauses(Clauses, Context, FeatureStore, Tag). 45 | 46 | %%=================================================================== 47 | %% Internal functions 48 | %%=================================================================== 49 | 50 | -spec new_from_template(map()) -> rule(). 51 | new_from_template(#{<<"id">> := Id, <<"clauses">> := Clauses, <<"trackEvents">> := TrackEvents, <<"variation">> := Variation}) -> 52 | #{id => Id, clauses => parse_clauses(Clauses), trackEvents => TrackEvents, variationOrRollout => Variation}; 53 | new_from_template(#{<<"id">> := Id, <<"clauses">> := Clauses, <<"trackEvents">> := TrackEvents, <<"rollout">> := #{ 54 | <<"variations">> := Variations 55 | } = Rollout}) when is_list(Variations) -> 56 | #{ 57 | id => Id, 58 | clauses => parse_clauses(Clauses), 59 | trackEvents => TrackEvents, 60 | variationOrRollout => ldclient_rollout:new(Rollout) 61 | }; 62 | new_from_template(#{<<"id">> := Id, <<"clauses">> := Clauses, <<"trackEvents">> := TrackEvents}) -> 63 | #{id => Id, clauses => parse_clauses(Clauses), trackEvents => TrackEvents, variationOrRollout => null}. 64 | 65 | -spec parse_clauses([map()]) -> [ldclient_clause:clause()]. 66 | parse_clauses(Clauses) -> 67 | F = fun(Clause) -> ldclient_clause:new(Clause) end, 68 | lists:map(F, Clauses). 69 | 70 | -spec check_clauses( 71 | Clauses:: [ldclient_clause:clause()], 72 | Context :: ldclient_context:context(), 73 | FeatureStore :: atom(), 74 | Tag :: atom()) -> match | no_match | malformed_flag. 75 | check_clauses([], _Context, _FeatureStore, _Tag) -> match; 76 | check_clauses([Clause|Rest], Context, FeatureStore, Tag) -> 77 | Result = ldclient_clause:match_context(Clause, Context, FeatureStore, Tag), 78 | check_clause_result(Result, Rest, Context, FeatureStore, Tag). 79 | 80 | -spec check_clause_result(Result :: match | no_match | malformed_flag, 81 | Clauses :: [ldclient_clause:clause()], 82 | Context :: ldclient_context:context(), 83 | FeatureStore :: atom(), 84 | Tag :: atom()) -> match | no_match | malformed_flag. 85 | check_clause_result(malformed_flag, _Rest, _Context, _FeatureStore, _Tag) -> malformed_flag; 86 | check_clause_result(no_match, _Rest, _Context, _FeatureStore, _Tag) -> no_match; 87 | check_clause_result(match, Rest, Context, FeatureStore, Tag) -> 88 | check_clauses(Rest, Context, FeatureStore, Tag). 89 | -------------------------------------------------------------------------------- /src/ldclient_storage_cache.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_storage_cache' module 3 | %% @private 4 | %% Provides implementation of storage cache using Erlang map. 5 | %% @end 6 | %%------------------------------------------------------------------- 7 | 8 | -module(ldclient_storage_cache). 9 | 10 | %% Helper macro for declaring children of supervisor 11 | -define(CHILD(Id, Module, Args, Type), {Id, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 12 | 13 | %% Behavior callbacks 14 | -export([init/3]). 15 | -export([create/4]). 16 | -export([empty/4]). 17 | -export([get/5]). 18 | -export([upsert/5]). 19 | -export([upsert_clean/5]). 20 | -export([delete/5]). 21 | -export([terminate/1]). 22 | 23 | %%=================================================================== 24 | %% Behavior callbacks 25 | %%=================================================================== 26 | 27 | -spec init(SupRef :: atom(), Tag :: atom(), Options :: list()) -> 28 | ok. 29 | init(SupRef, Tag, _) -> 30 | SupRegName = get_local_reg_name(supervisor, Tag), 31 | WorkerRegName = get_local_reg_name(worker, Tag), 32 | StorageCacheSup = ?CHILD(ldclient_storage_cache_sup, ldclient_storage_cache_sup, [SupRegName, WorkerRegName, Tag], supervisor), 33 | {ok, _} = supervisor:start_child(SupRef, StorageCacheSup), 34 | ok. 35 | 36 | -spec create(Tag :: atom(), Bucket :: atom(), ServerRef :: atom(), StorageBackend :: atom()) -> 37 | ok | 38 | {error, already_exists, string()}. 39 | create(Tag, Bucket, ServerRef, StorageBackend) -> 40 | LocalServerRef = get_local_reg_name(worker, Tag), 41 | _ = ldclient_storage_cache_server:create(LocalServerRef, Bucket), 42 | StorageBackend:create(ServerRef, Bucket). 43 | 44 | -spec empty(Tag :: atom(), Bucket :: atom(), ServerRef :: atom(), StorageBackend :: atom()) -> 45 | ok | 46 | {error, bucket_not_found, string()}. 47 | empty(Tag, Bucket, ServerRef, StorageBackend) -> 48 | LocalServerRef = get_local_reg_name(worker, Tag), 49 | ok = ldclient_storage_cache_server:empty(LocalServerRef, Bucket), 50 | StorageBackend:empty(ServerRef, Bucket). 51 | 52 | -spec get(Tag :: atom(), Bucket :: atom(), Key :: binary(), ServerRef :: atom(), StorageBackend :: atom()) -> 53 | [{Key :: binary(), Value :: any()}] | 54 | {error, bucket_not_found, string()}. 55 | get(Tag, Bucket, Key, ServerRef, StorageBackend) -> 56 | LocalServerRef = get_local_reg_name(worker, Tag), 57 | case ldclient_storage_cache_server:get(LocalServerRef, Bucket, Key) of 58 | {Value, Hit} -> case Hit of 59 | false -> StorageBackend:get(ServerRef, Bucket, Key); 60 | true -> Value 61 | end; 62 | {error, bucket_not_found, Error} -> {error, bucket_not_found, Error} 63 | end. 64 | 65 | -spec upsert(Tag :: atom(), Bucket :: atom(), Items :: #{Key :: binary() => Value :: any()}, ServerRef :: atom(), StorageBackend :: atom()) -> 66 | ok | 67 | {error, bucket_not_found, string()}. 68 | upsert(Tag, Bucket, Items, ServerRef, StorageBackend) -> 69 | LocalServerRef = get_local_reg_name(worker, Tag), 70 | ok = ldclient_storage_cache_server:upsert(LocalServerRef, Bucket, Items), 71 | StorageBackend:upsert(ServerRef, Bucket, Items). 72 | 73 | -spec upsert_clean(Tag :: atom(), Bucket :: atom(), Items :: #{Key :: binary() => Value :: any()}, ServerRef :: atom(), StorageBackend :: atom()) -> 74 | ok | 75 | {error, bucket_not_found, string()}. 76 | upsert_clean(Tag, Bucket, Items, ServerRef, StorageBackend) -> 77 | LocalServerRef = get_local_reg_name(worker, Tag), 78 | ok = ldclient_storage_cache_server:upsert_clean(LocalServerRef, Bucket, Items), 79 | StorageBackend:upsert_clean(ServerRef, Bucket, Items). 80 | 81 | -spec delete(Tag :: atom(), Bucket :: atom(), Key :: binary(), ServerRef :: atom(), StorageBackend :: atom()) -> 82 | ok | 83 | {error, bucket_not_found, string()}. 84 | delete(Tag, Bucket, Key, ServerRef, StorageBackend) -> 85 | LocalServerRef = get_local_reg_name(worker, Tag), 86 | ok =ldclient_storage_cache_server:delete(LocalServerRef, Bucket, Key), 87 | StorageBackend:delete(ServerRef, Bucket, Key). 88 | 89 | -spec terminate(Tag :: atom()) -> ok. 90 | terminate(_Tag) -> ok. 91 | 92 | %%=================================================================== 93 | %% Internal functions 94 | %%=================================================================== 95 | 96 | -spec get_local_reg_name(atom(), Tag :: atom()) -> atom(). 97 | get_local_reg_name(supervisor, Tag) -> 98 | list_to_atom("ldclient_storage_cache_sup_" ++ atom_to_list(Tag)); 99 | get_local_reg_name(worker, Tag) -> 100 | list_to_atom("ldclient_storage_cache_server_" ++ atom_to_list(Tag)). -------------------------------------------------------------------------------- /src/ldclient_storage_cache_sup.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_storage_cache_sup' module 3 | %% @private 4 | %% This is a supervisor for cache storage worker. 5 | %% @end 6 | %%------------------------------------------------------------------- 7 | 8 | -module(ldclient_storage_cache_sup). 9 | 10 | -behaviour(supervisor). 11 | 12 | %% Supervision 13 | -export([start_link/3, init/1]). 14 | 15 | %% Helper macro for declaring children of supervisor 16 | -define(CHILD(Id, Module, Args, Type), {Id, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 17 | 18 | %%=================================================================== 19 | %% Supervision 20 | %%=================================================================== 21 | 22 | -spec start_link(SupRegName :: atom(), WorkerRegName :: atom(), Tag :: atom()) -> 23 | {ok, Pid :: pid()} | ignore | {error, Reason :: term()}. 24 | start_link(SupRegName, WorkerRegName, Tag) when is_atom(SupRegName), is_atom(WorkerRegName), is_atom(Tag) -> 25 | supervisor:start_link({local, SupRegName}, ?MODULE, [WorkerRegName, Tag]). 26 | 27 | -spec init(Args :: term()) -> 28 | {ok, {{supervisor:strategy(), non_neg_integer(), pos_integer()}, [supervisor:child_spec()]}}. 29 | init([WorkerRegName, Tag]) -> 30 | {ok, {{one_for_one, 0, 1}, children(WorkerRegName, Tag)}}. 31 | 32 | %%=================================================================== 33 | %% Internal functions 34 | %%=================================================================== 35 | 36 | -spec children(WorkerRegName :: atom(), Tag :: atom()) -> [supervisor:child_spec()]. 37 | children(WorkerRegName, Tag) -> 38 | FeatureStorageServer = ?CHILD(ldclient_storage_cache_server, ldclient_storage_cache_server, [WorkerRegName, Tag], worker), 39 | [FeatureStorageServer]. 40 | -------------------------------------------------------------------------------- /src/ldclient_storage_engine.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_storage_engine' module 3 | %% @private 4 | %% This is a behavior that all storage engines must implement. It works with 5 | %% the concept of buckets and keys. 6 | %% @end 7 | %%------------------------------------------------------------------- 8 | 9 | -module(ldclient_storage_engine). 10 | 11 | %% Types 12 | -type event_operation() :: put | patch | delete. 13 | %% Operation for processing events. 14 | 15 | -export_type([event_operation/0]). 16 | 17 | %% `init' gets called during startup. Typically storage engine would initialize 18 | %% here. E.g. start application, supervisor, workers, establish connections, or 19 | %% any other initialization resources as needed. 20 | %% It is expected that `features' and `segments' buckets will be reachable after 21 | %% initialization, which should also be accounted for here. 22 | -callback init(SupRef :: atom(), Tag :: atom(), Options :: list()) -> 23 | ok. 24 | 25 | %% `create' must create a named bucket with a given atom. It must return 26 | %% `already_exists' error if the bucket by that name was previously created. 27 | -callback create(Tag :: atom(), Bucket :: atom()) -> 28 | ok 29 | | {error, already_exists, string()}. 30 | 31 | %% `empty' must delete all records from the specified bucket. 32 | %% If the bucket doesn't exist, it must return `bucket_not_found' error. 33 | -callback empty(Tag :: atom(), Bucket :: atom()) -> 34 | ok 35 | | {error, bucket_not_found, string()}. 36 | 37 | %% `get' must look up the given key in the bucket and return the result as a 38 | %% list of matching key-value pairs as tuples. If the bucket doesn't exist it 39 | %% must return `bucket_not_found' error. 40 | -callback get(Tag :: atom(), Bucket :: atom(), Key :: binary()) -> 41 | [{Key :: binary(), Value :: any()}] 42 | | {error, bucket_not_found, string()}. 43 | 44 | %% `all' must return all key-value pairs for the specified bucket as tuples. 45 | %% If the bucket doesn't exist, it must return `bucket_not_found' error. 46 | -callback all(Tag :: atom(), Bucket :: atom()) -> 47 | [{Key :: binary(), Value :: any()}] 48 | | {error, bucket_not_found, string()}. 49 | 50 | %% `upsert' must create or update key-value pair records in the given bucket. 51 | %% If the bucket doesn't exist, it must return `bucket_not_found' error. 52 | -callback upsert(Tag :: atom(), Bucket :: atom(), Item :: #{Key :: binary() => Value :: any()}) -> 53 | ok 54 | | {error, bucket_not_found, string()}. 55 | 56 | %% `upsert_clean' must perform `empty' and `upsert' atomically on a given bucket. 57 | -callback upsert_clean(Tag :: atom(), Bucket :: atom(), Item :: #{Key :: binary() => Value :: any()}) -> 58 | ok 59 | | {error, bucket_not_found, string()}. 60 | 61 | %% `delete` must delete a record from the specified bucket. 62 | -callback delete(Tag:: atom(), Bucket :: atom(), Key :: binary()) -> 63 | ok 64 | | {error, bucket_not_found, string()}. 65 | 66 | %% `terminate' is the opposite of `init'. It is expected to clean up any 67 | %% resources and fully shut down the storage backend. 68 | -callback terminate(Tag :: atom()) -> ok. 69 | -------------------------------------------------------------------------------- /src/ldclient_storage_ets.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_storage_ets' module 3 | %% @private 4 | %% Provides implementation of ETS storage backend behavior. 5 | %% @end 6 | %%------------------------------------------------------------------- 7 | 8 | -module(ldclient_storage_ets). 9 | 10 | -behaviour(ldclient_storage_engine). 11 | 12 | %% Helper macro for declaring children of supervisor 13 | -define(CHILD(Id, Module, Args, Type), {Id, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 14 | 15 | %% Behavior callbacks 16 | -export([init/3]). 17 | -export([create/2]). 18 | -export([empty/2]). 19 | -export([get/3]). 20 | -export([all/2]). 21 | -export([upsert/3]). 22 | -export([upsert_clean/3]). 23 | -export([delete/3]). 24 | -export([terminate/1]). 25 | 26 | %%=================================================================== 27 | %% Behavior callbacks 28 | %%=================================================================== 29 | 30 | -spec init(SupRef :: atom(), Tag :: atom(), Options :: list()) -> 31 | ok. 32 | init(SupRef, Tag, _) -> 33 | SupRegName = get_local_reg_name(supervisor, Tag), 34 | WorkerRegName = get_local_reg_name(worker, Tag), 35 | StorageSup = ?CHILD(ldclient_storage_ets_sup, ldclient_storage_ets_sup, [SupRegName, WorkerRegName, Tag], supervisor), 36 | {ok, _} = supervisor:start_child(SupRef, StorageSup), 37 | % Pre-create features and segments buckets 38 | ok = create(Tag, features), 39 | ok = create(Tag, segments), 40 | Reload = ldclient_update_processor_state:get_storage_initialized_state(Tag), 41 | case Reload of 42 | reload -> ok = ldclient_updater:stop(list_to_atom("ldclient_instance_stream_" ++ atom_to_list(Tag))); 43 | _ -> ok 44 | end. 45 | 46 | -spec create(Tag :: atom(), Bucket :: atom()) -> 47 | ok | 48 | {error, already_exists, string()}. 49 | create(Tag, Bucket) -> 50 | ServerRef = get_local_reg_name(worker, Tag), 51 | ldclient_storage_ets_server:create(ServerRef, Bucket). 52 | 53 | -spec empty(Tag :: atom(), Bucket :: atom()) -> 54 | ok | 55 | {error, bucket_not_found, string()}. 56 | empty(Tag, Bucket) -> 57 | ServerRef = get_local_reg_name(worker, Tag), 58 | ldclient_storage_ets_server:empty(ServerRef, Bucket). 59 | 60 | -spec get(Tag :: atom(), Bucket :: atom(), Key :: binary()) -> 61 | [{Key :: binary(), Value :: any()}] | 62 | {error, bucket_not_found, string()}. 63 | get(Tag, Bucket, Key) -> 64 | ServerRef = get_local_reg_name(worker, Tag), 65 | ldclient_storage_ets_server:get(ServerRef, Bucket, Key). 66 | 67 | -spec all(Tag :: atom(), Bucket :: atom()) -> 68 | [{Key :: binary(), Value :: any()}] | 69 | {error, bucket_not_found, string()}. 70 | all(Tag, Bucket) -> 71 | ServerRef = get_local_reg_name(worker, Tag), 72 | ldclient_storage_ets_server:all(ServerRef, Bucket). 73 | 74 | -spec upsert(Tag :: atom(), Bucket :: atom(), Items :: #{Key :: binary() => Value :: any()}) -> 75 | ok | 76 | {error, bucket_not_found, string()}. 77 | upsert(Tag, Bucket, Items) -> 78 | ServerRef = get_local_reg_name(worker, Tag), 79 | ldclient_storage_ets_server:upsert(ServerRef, Bucket, Items). 80 | 81 | -spec upsert_clean(Tag :: atom(), Bucket :: atom(), Items :: #{Key :: binary() => Value :: any()}) -> 82 | ok | 83 | {error, bucket_not_found, string()}. 84 | upsert_clean(Tag, Bucket, Items) -> 85 | ServerRef = get_local_reg_name(worker, Tag), 86 | ldclient_storage_ets_server:upsert_clean(ServerRef, Bucket, Items). 87 | 88 | -spec delete(Tag :: atom(), Bucket :: atom(), Key :: binary()) -> 89 | ok | 90 | {error, bucket_not_found, string()}. 91 | delete(Tag, Bucket, Key) -> 92 | ServerRef = get_local_reg_name(worker, Tag), 93 | ldclient_storage_ets_server:delete(ServerRef, Bucket, Key). 94 | 95 | -spec terminate(Tag :: atom()) -> ok. 96 | terminate(_Tag) -> ok. 97 | 98 | %%=================================================================== 99 | %% Internal functions 100 | %%=================================================================== 101 | 102 | -spec get_local_reg_name(atom(), Tag :: atom()) -> atom(). 103 | get_local_reg_name(supervisor, Tag) -> 104 | list_to_atom("ldclient_storage_ets_sup_" ++ atom_to_list(Tag)); 105 | get_local_reg_name(worker, Tag) -> 106 | list_to_atom("ldclient_storage_ets_server_" ++ atom_to_list(Tag)). 107 | -------------------------------------------------------------------------------- /src/ldclient_storage_ets_sup.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_storage_ets_sup' module 3 | %% @private 4 | %% This is a supervisor for ETS storage worker. 5 | %% @end 6 | %%------------------------------------------------------------------- 7 | 8 | -module(ldclient_storage_ets_sup). 9 | 10 | -behaviour(supervisor). 11 | 12 | %% Supervision 13 | -export([start_link/3, init/1]). 14 | 15 | %% Helper macro for declaring children of supervisor 16 | -define(CHILD(Id, Module, Args, Type), {Id, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 17 | 18 | %%=================================================================== 19 | %% Supervision 20 | %%=================================================================== 21 | 22 | -spec start_link(SupRegName :: atom(), WorkerRegName :: atom(), Tag :: atom()) -> 23 | {ok, Pid :: pid()} | ignore | {error, Reason :: term()}. 24 | start_link(SupRegName, WorkerRegName, Tag) when is_atom(SupRegName), is_atom(WorkerRegName), is_atom(Tag) -> 25 | supervisor:start_link({local, SupRegName}, ?MODULE, [WorkerRegName, Tag]). 26 | 27 | -spec init(Args :: term()) -> 28 | {ok, {{supervisor:strategy(), non_neg_integer(), pos_integer()}, [supervisor:child_spec()]}}. 29 | init([WorkerRegName, Tag]) -> 30 | {ok, {{one_for_one, 0, 1}, children(WorkerRegName, Tag)}}. 31 | 32 | %%=================================================================== 33 | %% Internal functions 34 | %%=================================================================== 35 | 36 | -spec children(WorkerRegName :: atom(), Tag :: atom()) -> [supervisor:child_spec()]. 37 | children(WorkerRegName, Tag) -> 38 | FeatureStorageServer = ?CHILD(ldclient_storage_ets_server, ldclient_storage_ets_server, [WorkerRegName, Tag], worker), 39 | [FeatureStorageServer]. 40 | -------------------------------------------------------------------------------- /src/ldclient_storage_map.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_storage_map' module 3 | %% @private 4 | %% Provides implementation of storage backend using Erlang map. 5 | %% @end 6 | %%------------------------------------------------------------------- 7 | 8 | -module(ldclient_storage_map). 9 | 10 | -behaviour(ldclient_storage_engine). 11 | 12 | %% Helper macro for declaring children of supervisor 13 | -define(CHILD(Id, Module, Args, Type), {Id, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 14 | 15 | %% Behavior callbacks 16 | -export([init/3]). 17 | -export([create/2]). 18 | -export([empty/2]). 19 | -export([get/3]). 20 | -export([all/2]). 21 | -export([upsert/3]). 22 | -export([upsert_clean/3]). 23 | -export([delete/3]). 24 | -export([terminate/1]). 25 | 26 | %%=================================================================== 27 | %% Behavior callbacks 28 | %%=================================================================== 29 | 30 | -spec init(SupRef :: atom(), Tag :: atom(), Options :: list()) -> 31 | ok. 32 | init(SupRef, Tag, _) -> 33 | SupRegName = get_local_reg_name(supervisor, Tag), 34 | WorkerRegName = get_local_reg_name(worker, Tag), 35 | StorageSup = ?CHILD(ldclient_storage_map_sup, ldclient_storage_map_sup, [SupRegName, WorkerRegName, Tag], supervisor), 36 | {ok, _} = supervisor:start_child(SupRef, StorageSup), 37 | % Pre-create features and segments buckets 38 | ok = create(Tag, features), 39 | ok = create(Tag, segments), 40 | Reload = ldclient_update_processor_state:get_storage_initialized_state(Tag), 41 | case Reload of 42 | reload -> ok = ldclient_updater:stop(list_to_atom("ldclient_instance_stream_" ++ atom_to_list(Tag))); 43 | _ -> ok 44 | end. 45 | 46 | -spec create(Tag :: atom(), Bucket :: atom()) -> 47 | ok | 48 | {error, already_exists, string()}. 49 | create(Tag, Bucket) -> 50 | ServerRef = get_local_reg_name(worker, Tag), 51 | ldclient_storage_map_server:create(ServerRef, Bucket). 52 | 53 | -spec empty(Tag :: atom(), Bucket :: atom()) -> 54 | ok | 55 | {error, bucket_not_found, string()}. 56 | empty(Tag, Bucket) -> 57 | ServerRef = get_local_reg_name(worker, Tag), 58 | ldclient_storage_map_server:empty(ServerRef, Bucket). 59 | 60 | -spec get(Tag :: atom(), Bucket :: atom(), Key :: binary()) -> 61 | [{Key :: binary(), Value :: any()}] | 62 | {error, bucket_not_found, string()}. 63 | get(Tag, Bucket, Key) -> 64 | ServerRef = get_local_reg_name(worker, Tag), 65 | ldclient_storage_map_server:get(ServerRef, Bucket, Key). 66 | 67 | -spec all(Tag :: atom(), Bucket :: atom()) -> 68 | [{Key :: binary(), Value :: any()}] | 69 | {error, bucket_not_found, string()}. 70 | all(Tag, Bucket) -> 71 | ServerRef = get_local_reg_name(worker, Tag), 72 | ldclient_storage_map_server:all(ServerRef, Bucket). 73 | 74 | -spec upsert(Tag :: atom(), Bucket :: atom(), Items :: #{Key :: binary() => Value :: any()}) -> 75 | ok | 76 | {error, bucket_not_found, string()}. 77 | upsert(Tag, Bucket, Items) -> 78 | ServerRef = get_local_reg_name(worker, Tag), 79 | ldclient_storage_map_server:upsert(ServerRef, Bucket, Items). 80 | 81 | -spec upsert_clean(Tag :: atom(), Bucket :: atom(), Items :: #{Key :: binary() => Value :: any()}) -> 82 | ok | 83 | {error, bucket_not_found, string()}. 84 | upsert_clean(Tag, Bucket, Items) -> 85 | ServerRef = get_local_reg_name(worker, Tag), 86 | ldclient_storage_map_server:upsert_clean(ServerRef, Bucket, Items). 87 | 88 | -spec delete(Tag :: atom(), Bucket :: atom(), Key :: binary()) -> 89 | ok | 90 | {error, bucket_not_found, string()}. 91 | delete(Tag, Bucket, Key) -> 92 | ServerRef = get_local_reg_name(worker, Tag), 93 | ldclient_storage_map_server:delete(ServerRef, Bucket, Key). 94 | 95 | -spec terminate(Tag :: atom()) -> ok. 96 | terminate(_Tag) -> ok. 97 | 98 | %%=================================================================== 99 | %% Internal functions 100 | %%=================================================================== 101 | 102 | -spec get_local_reg_name(atom(), Tag :: atom()) -> atom(). 103 | get_local_reg_name(supervisor, Tag) -> 104 | list_to_atom("ldclient_storage_map_sup_" ++ atom_to_list(Tag)); 105 | get_local_reg_name(worker, Tag) -> 106 | list_to_atom("ldclient_storage_map_server_" ++ atom_to_list(Tag)). 107 | -------------------------------------------------------------------------------- /src/ldclient_storage_map_sup.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_storage_map_sup' module 3 | %% @private 4 | %% This is a supervisor for map storage worker. 5 | %% @end 6 | %%------------------------------------------------------------------- 7 | 8 | -module(ldclient_storage_map_sup). 9 | 10 | -behaviour(supervisor). 11 | 12 | %% Supervision 13 | -export([start_link/3, init/1]). 14 | 15 | %% Helper macro for declaring children of supervisor 16 | -define(CHILD(Id, Module, Args, Type), {Id, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 17 | 18 | %%=================================================================== 19 | %% Supervision 20 | %%=================================================================== 21 | 22 | -spec start_link(SupRegName :: atom(), WorkerRegName :: atom(), Tag :: atom()) -> 23 | {ok, Pid :: pid()} | ignore | {error, Reason :: term()}. 24 | start_link(SupRegName, WorkerRegName, Tag) when is_atom(SupRegName), is_atom(WorkerRegName), is_atom(Tag) -> 25 | supervisor:start_link({local, SupRegName}, ?MODULE, [WorkerRegName, Tag]). 26 | 27 | -spec init(Args :: term()) -> 28 | {ok, {{supervisor:strategy(), non_neg_integer(), pos_integer()}, [supervisor:child_spec()]}}. 29 | init([WorkerRegName, Tag]) -> 30 | {ok, {{one_for_one, 0, 1}, children(WorkerRegName, Tag)}}. 31 | 32 | %%=================================================================== 33 | %% Internal functions 34 | %%=================================================================== 35 | 36 | -spec children(WorkerRegName :: atom(), Tag :: atom()) -> [supervisor:child_spec()]. 37 | children(WorkerRegName, Tag) -> 38 | FeatureStorageServer = ?CHILD(ldclient_storage_map_server, ldclient_storage_map_server, [WorkerRegName, Tag], worker), 39 | [FeatureStorageServer]. 40 | -------------------------------------------------------------------------------- /src/ldclient_storage_redis.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_storage_redis' module 3 | %% @private 4 | %% Provides implementation of Redis storage backend behavior. 5 | %% @end 6 | %%------------------------------------------------------------------- 7 | 8 | -module(ldclient_storage_redis). 9 | 10 | -behaviour(ldclient_storage_engine). 11 | 12 | %% Helper macro for declaring children of supervisor 13 | -define(CHILD(Id, Module, Args, Type), {Id, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 14 | 15 | %% Behavior callbacks 16 | -export([init/3]). 17 | -export([create/2]). 18 | -export([empty/2]). 19 | -export([get/3]). 20 | -export([all/2]). 21 | -export([upsert/3]). 22 | -export([upsert_clean/3]). 23 | -export([delete/3]). 24 | -export([terminate/1]). 25 | -export([set_init/1]). 26 | -export([get_init/1]). 27 | 28 | %%=================================================================== 29 | %% Behavior callbacks 30 | %%=================================================================== 31 | 32 | -spec init(SupRef :: atom(), Tag :: atom(), Options :: list()) -> 33 | ok. 34 | init(SupRef, Tag, _) -> 35 | SupRegName = get_local_reg_name(supervisor, Tag), 36 | WorkerRegName = get_local_reg_name(worker, Tag), 37 | StorageSup = ?CHILD(ldclient_storage_redis_sup, ldclient_storage_redis_sup, [SupRegName, WorkerRegName, Tag], supervisor), 38 | {ok, _} = supervisor:start_child(SupRef, StorageSup), 39 | ok = ldclient_storage_cache:init(SupRef, Tag, []), 40 | % Pre-create features and segments buckets 41 | ok = create(Tag, features), 42 | ok = create(Tag, segments). 43 | 44 | -spec create(Tag :: atom(), Bucket :: atom()) -> 45 | ok | 46 | {error, already_exists, string()}. 47 | create(Tag, Bucket) -> 48 | ServerRef = get_local_reg_name(worker, Tag), 49 | ldclient_storage_cache:create(Tag, Bucket, ServerRef, ldclient_storage_redis_server). 50 | 51 | -spec empty(Tag :: atom(), Bucket :: atom()) -> 52 | ok | 53 | {error, bucket_not_found, string()}. 54 | empty(Tag, Bucket) -> 55 | ServerRef = get_local_reg_name(worker, Tag), 56 | ldclient_storage_cache:empty(Tag, Bucket, ServerRef, ldclient_storage_redis_server). 57 | 58 | -spec get(Tag :: atom(), Bucket :: atom(), Key :: binary()) -> 59 | [{Key :: binary(), Value :: any()}] | 60 | {error, bucket_not_found, string()}. 61 | get(Tag, Bucket, Key) -> 62 | ServerRef = get_local_reg_name(worker, Tag), 63 | ldclient_storage_cache:get(Tag, Bucket, Key, ServerRef, ldclient_storage_redis_server). 64 | 65 | -spec all(Tag :: atom(), Bucket :: atom()) -> 66 | [{Key :: binary(), Value :: any()}] | 67 | {error, bucket_not_found, string()}. 68 | all(Tag, Bucket) -> 69 | ServerRef = get_local_reg_name(worker, Tag), 70 | ldclient_storage_redis_server:all(ServerRef, Bucket). 71 | 72 | -spec upsert(Tag :: atom(), Bucket :: atom(), Items :: #{Key :: binary() => Value :: any()}) -> 73 | ok | 74 | {error, bucket_not_found, string()}. 75 | upsert(Tag, Bucket, Items) -> 76 | ServerRef = get_local_reg_name(worker, Tag), 77 | ldclient_storage_cache:upsert(Tag, Bucket, Items, ServerRef, ldclient_storage_redis_server). 78 | 79 | -spec upsert_clean(Tag :: atom(), Bucket :: atom(), Items :: #{Key :: binary() => Value :: any()}) -> 80 | ok | 81 | {error, bucket_not_found, string()}. 82 | upsert_clean(Tag, Bucket, Items) -> 83 | ServerRef = get_local_reg_name(worker, Tag), 84 | ldclient_storage_cache:upsert_clean(Tag, Bucket, Items, ServerRef, ldclient_storage_redis_server). 85 | 86 | set_init(Tag) -> 87 | ServerRef = get_local_reg_name(worker, Tag), 88 | ldclient_storage_redis_server:set_init(ServerRef). 89 | 90 | get_init(Tag) -> 91 | ServerRef = get_local_reg_name(worker, Tag), 92 | Initialized = ldclient_storage_redis_server:get_init(ServerRef), 93 | ldclient_update_processor_state:set_storage_initialized_state(Tag, Initialized), 94 | Initialized. 95 | 96 | -spec delete(Tag :: atom(), Bucket :: atom(), Key :: binary()) -> 97 | ok | 98 | {error, bucket_not_found, string()}. 99 | delete(Tag, Bucket, Key) -> 100 | ServerRef = get_local_reg_name(worker, Tag), 101 | ldclient_storage_cache:delete(Tag, Bucket, Key, ServerRef, ldclient_storage_redis_server). 102 | 103 | -spec terminate(Tag :: atom()) -> ok. 104 | terminate(_Tag) -> ok. 105 | 106 | %%=================================================================== 107 | %% Internal functions 108 | %%=================================================================== 109 | 110 | -spec get_local_reg_name(atom(), Tag :: atom()) -> atom(). 111 | get_local_reg_name(supervisor, Tag) -> 112 | list_to_atom("ldclient_storage_redis_sup_" ++ atom_to_list(Tag)); 113 | get_local_reg_name(worker, Tag) -> 114 | list_to_atom("ldclient_storage_redis_server_" ++ atom_to_list(Tag)). 115 | -------------------------------------------------------------------------------- /src/ldclient_storage_redis_sup.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_storage_redis_sup' module 3 | %% @private 4 | %% This is a supervisor for Redis storage worker. 5 | %% @end 6 | %%------------------------------------------------------------------- 7 | 8 | -module(ldclient_storage_redis_sup). 9 | 10 | -behaviour(supervisor). 11 | 12 | %% Supervision 13 | -export([start_link/3, init/1]). 14 | 15 | %% Helper macro for declaring children of supervisor 16 | -define(CHILD(Id, Module, Args, Type), {Id, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 17 | 18 | %%=================================================================== 19 | %% Supervision 20 | %%=================================================================== 21 | 22 | -spec start_link(SupRegName :: atom(), WorkerRegName :: atom(), Tag :: atom()) -> 23 | {ok, Pid :: pid()} | ignore | {error, Reason :: term()}. 24 | start_link(SupRegName, WorkerRegName, Tag) when is_atom(SupRegName), is_atom(WorkerRegName), is_atom(Tag) -> 25 | supervisor:start_link({local, SupRegName}, ?MODULE, [WorkerRegName, Tag]). 26 | 27 | -spec init(Args :: term()) -> 28 | {ok, {{supervisor:strategy(), non_neg_integer(), pos_integer()}, [supervisor:child_spec()]}}. 29 | init([WorkerRegName, Tag]) -> 30 | {ok, {{one_for_one, 0, 1}, children(WorkerRegName, Tag)}}. 31 | 32 | %%=================================================================== 33 | %% Internal functions 34 | %%=================================================================== 35 | 36 | -spec children(WorkerRegName :: atom(), Tag :: atom()) -> [supervisor:child_spec()]. 37 | children(WorkerRegName, Tag) -> 38 | FeatureStorageServer = ?CHILD(ldclient_storage_redis_server, ldclient_storage_redis_server, [WorkerRegName, Tag], worker), 39 | [FeatureStorageServer]. 40 | -------------------------------------------------------------------------------- /src/ldclient_sup.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Top level application supervisor 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_sup). 8 | 9 | -behaviour(supervisor). 10 | 11 | %% Supervision 12 | -export([start_link/0, init/1]). 13 | 14 | %%=================================================================== 15 | %% Supervision 16 | %%=================================================================== 17 | 18 | -spec(start_link() -> 19 | {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). 20 | start_link() -> 21 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 22 | 23 | -spec init(Args :: term()) -> 24 | {ok, {{supervisor:strategy(), non_neg_integer(), pos_integer()}, [supervisor:child_spec()]}}. 25 | init([]) -> 26 | MaxRestart = 10, 27 | MaxTime = 3600, 28 | {ok, {{one_for_one, MaxRestart, MaxTime}, []}}. 29 | -------------------------------------------------------------------------------- /src/ldclient_targets.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Methods for evaluating targets. 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_targets). 8 | 9 | %% API 10 | -export([eval_flag_targets/2]). 11 | 12 | -type match_result() :: no_match | {match, non_neg_integer()}. 13 | 14 | %% @doc Evaluate the targets of the flag against the specified context. 15 | %% 16 | %% If there are no matches, then return `no_match'. 17 | %% 18 | %% If there is a match, then return `{match, Variation}'. 19 | %% @end 20 | -spec eval_flag_targets(Flag :: ldclient_flag:flag(), Context :: ldclient_context:context()) -> match_result(). 21 | eval_flag_targets( 22 | #{targets := [], contextTargets := []} = _Flag, 23 | _Context 24 | ) -> 25 | %% There are no targets to evaluate, so there is no match. 26 | no_match; 27 | eval_flag_targets( 28 | #{targets := UserTargets, contextTargets := []} = _Flag, 29 | Context 30 | ) -> 31 | %% The context targets are empty, so evaluate user targets only. 32 | check_user_targets(UserTargets, Context); 33 | eval_flag_targets( 34 | #{targets := UserTargets, contextTargets := ContextTargets} = _Flag, 35 | Context 36 | ) -> 37 | check_context_targets(ContextTargets, UserTargets, Context). 38 | 39 | %%=================================================================== 40 | %% Internal functions 41 | %%=================================================================== 42 | 43 | %% @doc Check context targets for any matches. 44 | %% 45 | %% This logic is independent of user targets checking because context targets have special logic 46 | %% for when their kind is "user". The values list will be empty, and instead the user targets should 47 | %% be checked for a target of the same variation. This allows for user targets to remain compatible 48 | %% with existing SDKs and also not need to be duplicated for context targets. 49 | %% @end 50 | -spec check_context_targets( 51 | ContextTargets :: [ldclient_flag:target()], 52 | UserTargets :: [ldclient_flag:target()], 53 | Context :: ldclient_context:context() 54 | ) -> match_result(). 55 | check_context_targets([], _UserTargets, _Context) -> no_match; 56 | check_context_targets([#{contextKind := ContextKind, variation := Variation} = ContextTarget|Rest] = _Target, UserTargets, Context) -> 57 | Result = case ContextKind of 58 | %% In the case the context targets are for a user kind, then we are just using it for ordering, and we want 59 | %% To check a user target with the same variation as the context target. 60 | <<"user">> -> eval_target(find_user_target_for_variation(Variation, UserTargets), Context); 61 | %% This was not a user kind, so just evaluate the target. 62 | _ -> eval_target(ContextTarget, Context) 63 | end, 64 | %% If there is no match, then continue checking, if there is a match, then stop early and return it. 65 | case Result of 66 | no_match ->check_context_targets(Rest, UserTargets, Context); 67 | _ -> Result 68 | end. 69 | 70 | %% @doc Check the user targets of the flag to see if there are any matches. 71 | %% 72 | %% @end 73 | -spec check_user_targets(UserTargets :: [ldclient_flag:target()], Context :: ldclient_context:context()) -> match_result(). 74 | check_user_targets([], _Context) -> 75 | no_match; 76 | check_user_targets([Target|Rest] = _Targets, Context) -> 77 | case eval_target(Target, Context) of 78 | %% There was not a match, so continue checking. 79 | no_match -> check_user_targets(Rest, Context); 80 | %% There was a match, so return it and stop checking. 81 | Match -> Match 82 | end. 83 | 84 | %% @doc Find a user target associated with a given variation. 85 | %% 86 | %% If there is no user target with a matching variation, then null will be returned. 87 | %% 88 | %% If there is a user target with a matching variation, then that target will be returned. 89 | %% @end 90 | -spec find_user_target_for_variation( 91 | Variation :: non_neg_integer(), 92 | UserTargets :: [ldclient_flag:target()] 93 | ) -> ldclient_flag:target() | null. 94 | find_user_target_for_variation(Variation, UserTargets) -> 95 | find_user_target_for_variation_result(lists:search(fun(Target) -> 96 | #{variation := TargetVariation} = Target, 97 | TargetVariation =:= Variation 98 | end, UserTargets)). 99 | 100 | %% @doc Convenience method to convert a lists:search result into either null or a target(). 101 | %% 102 | %% @end 103 | -spec find_user_target_for_variation_result(Result :: false | {value, ldclient_flag:target()}) -> null | ldclient_flag:target(). 104 | find_user_target_for_variation_result(false) -> null; 105 | find_user_target_for_variation_result({value, Value}) -> Value. 106 | 107 | %% @doc Evaluate a single target against a context. 108 | %% 109 | %% @end 110 | -spec eval_target(Target :: null | ldclient_flag:target(), Context :: ldclient_context:context()) -> match_result(). 111 | eval_target(null, _Context) -> no_match; 112 | eval_target( 113 | #{contextKind := ContextKind, values := Values, variation := Variation} = _Target, 114 | Context 115 | ) -> 116 | Key = ldclient_context:get_key(ContextKind, Context), 117 | Match = eval_target_key(Key, Values), 118 | eval_target_result(Match, Variation). 119 | 120 | %% @doc Convenience method to search for a key in a list of values. If the key is null, then it returns 121 | %% false instead of performing the search. 122 | %% @end 123 | -spec eval_target_key(Key :: null | binary(), Values :: [binary()]) -> boolean(). 124 | eval_target_key(null = _Key, _Values) -> false; 125 | eval_target_key(Key, Values) -> lists:member(Key, Values). 126 | 127 | %% @doc Convenience method for turning a search result, and a variation into a match_result(). 128 | %% 129 | %% @end 130 | -spec eval_target_result(Match :: boolean(), Variation :: pos_integer()) -> match_result(). 131 | eval_target_result(false = _Match, _Variation) -> no_match; 132 | eval_target_result(true = _Match, Variation) -> {match, Variation}. 133 | -------------------------------------------------------------------------------- /src/ldclient_time.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Local module for erlang module time methods. 3 | %% This allows them to be easily mocked for testing. 4 | %% @private 5 | %% @end 6 | %%------------------------------------------------------------------- 7 | -module(ldclient_time). 8 | 9 | %% API 10 | -export([ 11 | time_seconds/0, 12 | start_timer/3, 13 | datetime_to_timestamp/1 14 | ]). 15 | 16 | %% The difference between the start of the Gregorian calendar and 1/1/1970 (Unix Epoch). 17 | -define(GREGORIAN_UNIX_OFFSET_SECONDS, 62167219200). 18 | 19 | -spec time_seconds() -> integer(). 20 | time_seconds() -> 21 | erlang:monotonic_time(second). 22 | 23 | -spec start_timer(Time :: non_neg_integer(), Dest :: pid() | atom(), Msg :: term()) -> Timer :: reference(). 24 | start_timer(Time, Dest, Msg) -> 25 | erlang:start_timer(Time, Dest, Msg). 26 | 27 | %% There isn't a built-in method to convert from a calendar:datetime() to a unix timestamp. 28 | %% This is useful to allow comparison between a parsed date and erlang:system_time. 29 | -spec datetime_to_timestamp(DateTime :: calendar:datetime()) -> UtcMilliseconds :: integer(). 30 | datetime_to_timestamp(DateTime) -> 31 | (calendar:datetime_to_gregorian_seconds(DateTime) - ?GREGORIAN_UNIX_OFFSET_SECONDS) * 1000. 32 | -------------------------------------------------------------------------------- /src/ldclient_update_null_server.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Null update server 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_update_null_server). 8 | 9 | -behaviour(gen_server). 10 | 11 | %% Supervision 12 | -export([start_link/1, init/1]). 13 | 14 | %% Behavior callbacks 15 | -export([code_change/3, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). 16 | 17 | -type state() :: #{}. 18 | 19 | -ifdef(TEST). 20 | -compile(export_all). 21 | -endif. 22 | 23 | %%=================================================================== 24 | %% Supervision 25 | %%=================================================================== 26 | 27 | %% @doc Starts the server 28 | %% 29 | %% @end 30 | -spec start_link(Tag :: atom()) -> 31 | {ok, Pid :: pid()} | ignore | {error, Reason :: term()}. 32 | start_link(Tag) -> 33 | error_logger:info_msg("Starting null update server for ~p", [Tag]), 34 | gen_server:start_link(?MODULE, [Tag], []). 35 | 36 | -spec init(Args :: term()) -> 37 | {ok, State :: state()} | {ok, State :: state(), timeout() | hibernate} | 38 | {stop, Reason :: term()} | ignore. 39 | init([_Tag]) -> 40 | % Need to trap exit so supervisor:terminate_child calls terminate callback 41 | process_flag(trap_exit, true), 42 | State = #{}, 43 | {ok, State}. 44 | 45 | %%=================================================================== 46 | %% Behavior callbacks 47 | %%=================================================================== 48 | 49 | -type from() :: {pid(), term()}. 50 | -spec handle_call(Request :: term(), From :: from(), State :: state()) -> 51 | {reply, Reply :: term(), NewState :: state()} | 52 | {stop, normal, {error, atom(), term()}, state()}. 53 | handle_call(_Request, _From, State) -> 54 | {reply, ok, State}. 55 | 56 | handle_cast(_Request, State) -> 57 | {noreply, State}. 58 | 59 | handle_info(_Info, State) -> 60 | {noreply, State}. 61 | 62 | -spec terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), 63 | State :: state()) -> term(). 64 | terminate(Reason, _State) -> 65 | error_logger:info_msg("Terminating, reason: ~p; Pid none~n", [Reason]), 66 | ok. 67 | 68 | code_change(_OldVsn, State, _Extra) -> 69 | {ok, State}. 70 | -------------------------------------------------------------------------------- /src/ldclient_update_processor_state.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_update_processor_state' module 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_update_processor_state). 8 | 9 | % API 10 | -export([init/0]). 11 | -export([create_initialized_state/2]). 12 | -export([delete_initialized_state/1]). 13 | -export([set_initialized_state/2]). 14 | -export([get_initialized_state/1]). 15 | -export([create_storage_initialized_state/2]). 16 | -export([delete_storage_initialized_state/1]). 17 | -export([set_storage_initialized_state/2]). 18 | -export([get_storage_initialized_state/1]). 19 | 20 | -define(UPDATE_TABLE, ldclient_update_processor_initialization). 21 | 22 | %%=================================================================== 23 | %% API 24 | %%=================================================================== 25 | 26 | %% @doc Initialize update processor ETS table 27 | %% 28 | %% Initializes an empty ETS table for initialization state of update processors. 29 | %% @end 30 | -spec init() -> ok. 31 | init() -> 32 | _UTID = ets:new(?UPDATE_TABLE, [set, public, named_table, {read_concurrency, true}]), 33 | ok. 34 | 35 | %% @doc Create a state for a new processor 36 | %% 37 | %% @end 38 | -spec create_initialized_state(Tag :: atom(), Value :: boolean()) -> boolean(). 39 | create_initialized_state(Tag, Value) -> 40 | ets:insert_new(?UPDATE_TABLE, {Tag, Value}). 41 | 42 | %% @doc Delete initialized state for a processor 43 | %% 44 | %% @end 45 | -spec delete_initialized_state(Tag :: atom()) -> boolean(). 46 | delete_initialized_state(Tag) -> 47 | ets:delete(?UPDATE_TABLE, Tag). 48 | 49 | %% @doc Change the update processor status for a given Tag 50 | %% 51 | %% @end 52 | -spec set_initialized_state(Tag :: atom(), Value :: boolean()) -> boolean(). 53 | set_initialized_state(Tag, Value) -> 54 | ets:insert(?UPDATE_TABLE, {Tag, Value}). 55 | 56 | %% @doc Return the update processor status for a given Tag 57 | %% 58 | %% @end 59 | -spec get_initialized_state(Tag :: atom()) -> boolean(). 60 | get_initialized_state(Tag) -> 61 | [{_Tag, Initialized}] = ets:lookup(?UPDATE_TABLE, Tag), 62 | Initialized. 63 | 64 | %% @doc Create a state for a new storage server 65 | %% 66 | %% @end 67 | -spec create_storage_initialized_state(Tag :: atom(), Value :: atom()) -> boolean(). 68 | create_storage_initialized_state(Tag, Value) -> 69 | ets:insert_new(?UPDATE_TABLE, {get_storage_name(Tag), Value}). 70 | 71 | %% @doc Delete initialized state for a storage server 72 | %% 73 | %% @end 74 | -spec delete_storage_initialized_state(Tag :: atom()) -> boolean(). 75 | delete_storage_initialized_state(Tag) -> 76 | ets:delete(?UPDATE_TABLE, get_storage_name(Tag)). 77 | 78 | %% @doc Change the storage server status for a given Tag 79 | %% 80 | %% @end 81 | -spec set_storage_initialized_state(Tag :: atom(), Value :: atom()) -> boolean(). 82 | set_storage_initialized_state(Tag, Value) -> 83 | ets:insert(?UPDATE_TABLE, {get_storage_name(Tag), Value}). 84 | 85 | %% @doc Return the storage server status for a given Tag 86 | %% 87 | %% @end 88 | -spec get_storage_initialized_state(Tag :: atom()) -> atom(). 89 | get_storage_initialized_state(Tag) -> 90 | [{_Tag, Initialized}] = ets:lookup(?UPDATE_TABLE, get_storage_name(Tag)), 91 | Initialized. 92 | 93 | -spec get_storage_name(Tag :: atom()) -> atom(). 94 | get_storage_name(Tag) -> 95 | list_to_atom(atom_to_list(Tag) ++ atom_to_list(ldclient_config:get_value(Tag, feature_store))). -------------------------------------------------------------------------------- /src/ldclient_update_requestor.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_update_requestor' module 3 | %% @private 4 | %% This is a behavior that event dispatchers must implement. It is used to send 5 | %% event batches to LaunchDarkly. 6 | %% @end 7 | %%------------------------------------------------------------------- 8 | 9 | -module(ldclient_update_requestor). 10 | 11 | -type response() :: {ok, binary() | not_modified} 12 | | {error, errors()}. 13 | 14 | -type errors() :: {bad_status, non_neg_integer(), string()} 15 | | network_error. 16 | 17 | -export_type([response/0, errors/0]). 18 | 19 | %% `all' must request and return all flags and segments, along with an updated 20 | %% state value. It takes the destination URI, the SDK key, along with a state 21 | %% value from `init' or the previous invocation of `all' to allow for features 22 | %% such as ETag caching. 23 | -callback all(Uri :: string(), State :: any()) -> {response(), any()}. 24 | 25 | %% `init' should return an initial value for the `State' argument to `all' 26 | -callback init(Tag :: atom(), SdkKey :: string()) -> any(). 27 | -------------------------------------------------------------------------------- /src/ldclient_update_requestor_httpc.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Polling update requestor 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_update_requestor_httpc). 8 | 9 | -behaviour(ldclient_update_requestor). 10 | 11 | %% Behavior callbacks 12 | -export([init/2, all/2]). 13 | 14 | %% Internal type for ETag cache state 15 | -type state() :: #{ 16 | etag_state => #{string() => binary()}, 17 | headers => list(), 18 | http_options => list() 19 | }. 20 | 21 | %%=================================================================== 22 | %% Behavior callbacks 23 | %%=================================================================== 24 | 25 | -spec init(Tag :: atom(), SdkKey :: string()) -> state(). 26 | init(Tag, _SdkKey) -> 27 | Options = ldclient_config:get_value(Tag, http_options), 28 | HttpOptions = ldclient_http_options:httpc_parse_http_options(Options), 29 | DefaultHeaders = ldclient_headers:get_default_headers(Tag, string_pairs), 30 | Headers = ldclient_http_options:httpc_append_custom_headers([ 31 | {"X-LaunchDarkly-Event-Schema", ldclient_config:get_event_schema()} 32 | | DefaultHeaders 33 | ], Options), 34 | #{ 35 | %% Initializes ETag cache to empty map 36 | etag_state => #{}, 37 | headers => Headers, 38 | http_options => HttpOptions 39 | }. 40 | 41 | %% @doc Send events to LaunchDarkly event server 42 | %% 43 | %% @end 44 | -spec all(Uri :: string(), State :: state()) -> 45 | {ldclient_update_requestor:response(), state()}. 46 | all(Uri, State) -> 47 | #{etag_state := EtagState, headers := Headers, http_options := HttpOptions} = State, 48 | ETagHeaders = case maps:find(Uri, EtagState) of 49 | error -> Headers; 50 | {ok, Etag} -> [{"If-None-Match", Etag}|Headers] 51 | end, 52 | Result = httpc:request(get, {Uri, ETagHeaders}, HttpOptions, [{body_format, binary}]), 53 | case Result of 54 | {ok, {StatusLine = {_, StatusCode, _}, ResponseHeaders, Body}} -> 55 | if 56 | StatusCode =:= 304 -> {{ok, not_modified}, State}; 57 | StatusCode < 300 -> 58 | case proplists:lookup("etag", [{string:casefold(K), V} || {K, V} <- ResponseHeaders]) of 59 | none -> {{ok, Body}, State}; 60 | {_, ETag} -> {{ok, Body}, State#{etag_state => EtagState#{Uri => ETag}}} 61 | end; 62 | StatusCode < 400 -> {{ok, Body}, State}; 63 | true -> {{error, bad_status_error(StatusLine)}, State} 64 | end; 65 | _ -> {{error, network_error}, State} 66 | end. 67 | 68 | %%=================================================================== 69 | %% Internal functions 70 | %%=================================================================== 71 | 72 | -spec bad_status_error(StatusLine :: { 73 | uri_string:uri_string(), 74 | non_neg_integer(), 75 | string() 76 | }) -> ldclient_update_requestor:errors(). 77 | bad_status_error({Version, StatusCode, ReasonPhrase}) -> 78 | {bad_status, StatusCode, io_lib:format("~s ~b ~s", [Version, StatusCode, ReasonPhrase])}. 79 | -------------------------------------------------------------------------------- /src/ldclient_update_sup.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Update processor supervisor 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_update_sup). 8 | 9 | -behaviour(supervisor). 10 | 11 | %% Supervision 12 | -export([start_link/2, init/1]). 13 | 14 | %%=================================================================== 15 | %% Supervision 16 | %%=================================================================== 17 | 18 | -spec start_link(UpdateSupName :: atom(), UpdateWorkerModule :: atom()) -> 19 | {ok, Pid :: pid()} | ignore | {error, Reason :: term()}. 20 | start_link(UpdateSupName, UpdateWorkerModule) -> 21 | supervisor:start_link({local, UpdateSupName}, ?MODULE, [UpdateWorkerModule]). 22 | 23 | -spec init(Args :: term()) -> 24 | {ok, {{supervisor:strategy(), non_neg_integer(), pos_integer()}, [supervisor:child_spec()]}}. 25 | init([UpdateWorkerModule]) -> 26 | MaxRestart = 10, 27 | MaxTime = 3600, 28 | ChildSpec = { 29 | UpdateWorkerModule, 30 | {UpdateWorkerModule, start_link, []}, 31 | permanent, 32 | 5000, % shutdown time 33 | worker, 34 | [UpdateWorkerModule] 35 | }, 36 | {ok, {{simple_one_for_one, MaxRestart, MaxTime}, [ChildSpec]}}. 37 | -------------------------------------------------------------------------------- /src/ldclient_update_testdata_server.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Testdata update server 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_update_testdata_server). 8 | 9 | -behaviour(gen_server). 10 | 11 | %% Supervision 12 | -export([start_link/1, init/1]). 13 | 14 | %% Behavior callbacks 15 | -export([code_change/3, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). 16 | 17 | -type state() :: #{ 18 | tag => atom(), 19 | test_data_server => atom() 20 | }. 21 | 22 | -ifdef(TEST). 23 | -compile(export_all). 24 | -endif. 25 | 26 | %%=================================================================== 27 | %% Supervision 28 | %%=================================================================== 29 | 30 | %% @doc Starts the server 31 | %% 32 | %% @end 33 | -spec start_link(Tag :: atom()) -> 34 | {ok, Pid :: pid()} | ignore | {error, Reason :: term()}. 35 | start_link(Tag) -> 36 | error_logger:info_msg("Starting testdata update server for ~p", [Tag]), 37 | gen_server:start_link(?MODULE, [Tag], []). 38 | 39 | -spec init(Args :: term()) -> 40 | {ok, State :: state()} | {ok, State :: state(), timeout() | hibernate} | 41 | {stop, Reason :: term()} | ignore. 42 | init([Tag]) -> 43 | % Need to trap exit so supervisor:terminate_child calls terminate callback 44 | process_flag(trap_exit, true), 45 | TestDataInstanceTag = ldclient_config:get_value(Tag, testdata_tag), 46 | TestDataServer = case supervisor:start_child(ldclient_sup, ldclient_testdata:child_spec(TestDataInstanceTag, [])) of 47 | {ok, Child} -> Child; 48 | {error, { already_started, Child}} -> Child 49 | end, 50 | State = #{ 51 | tag => Tag, 52 | test_data_server => TestDataServer 53 | }, 54 | gen_server:call(TestDataServer, {register_instance, Tag}), 55 | {ok, State}. 56 | 57 | %%=================================================================== 58 | %% Behavior callbacks 59 | %%=================================================================== 60 | 61 | -type from() :: {pid(), term()}. 62 | -spec handle_call(Request :: term(), From :: from(), State :: state()) -> 63 | {reply, Reply :: term(), NewState :: state()} | 64 | {stop, normal, {error, atom(), term()}, state()}. 65 | handle_call(_Request, _From, State) -> 66 | {reply, ok, State}. 67 | 68 | handle_cast(_Request, State) -> 69 | {noreply, State}. 70 | 71 | handle_info(_Info, State) -> 72 | {noreply, State}. 73 | 74 | -spec terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), 75 | State :: state()) -> term(). 76 | terminate(Reason, #{ tag := Tag, test_data_server := TestDataServer }) -> 77 | error_logger:info_msg("Terminating, reason: ~p; Pid: ~p ~n", [Reason, self()]), 78 | gen_server:call(TestDataServer, {unregister_instance, Tag}), 79 | ok. 80 | 81 | code_change(_OldVsn, State, _Extra) -> 82 | {ok, State}. 83 | -------------------------------------------------------------------------------- /src/ldclient_updater.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ldclient_updater' module 3 | %% @private 4 | %% Used to start and stop client stream listener. 5 | %% @end 6 | %%------------------------------------------------------------------- 7 | 8 | -module(ldclient_updater). 9 | 10 | %% API 11 | -export([start/3]). 12 | -export([stop/1]). 13 | 14 | %% Constants 15 | -define(CHILD(Id, Module, Args, Type), {Id, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 16 | 17 | %%=================================================================== 18 | %% API 19 | %%=================================================================== 20 | 21 | %% @doc Start client stream listener 22 | %% 23 | %% @end 24 | -spec start(UpdateSupName :: atom(), UpdateWorkerModule :: atom(), Tag :: atom()) -> 25 | {ok, pid()}. 26 | start(UpdateSupName, UpdateWorkerModule, Tag) when is_atom(UpdateSupName), is_atom(UpdateWorkerModule) -> 27 | {ok, _Pid} = supervisor:start_child(UpdateSupName, [Tag]). 28 | 29 | %% @doc Stop client stream listener 30 | %% 31 | %% @end 32 | -spec stop(Tag :: atom()) -> ok. 33 | stop(StreamSupName) when is_atom(StreamSupName) -> 34 | ok = terminate_all_children(StreamSupName). 35 | 36 | %%=================================================================== 37 | %% Internal functions 38 | %%=================================================================== 39 | 40 | %% @doc Terminates all children of a given supervisor 41 | %% @private 42 | %% 43 | %% @end 44 | -spec terminate_all_children(Sup :: atom()) -> ok. 45 | terminate_all_children(Sup) -> 46 | Pids = [Pid || {_, Pid, worker, _} <- supervisor:which_children(Sup)], 47 | terminate_all_children(Sup, Pids). 48 | 49 | %% @doc Recursively terminates processes of given children Pids 50 | %% @private 51 | %% 52 | %% @end 53 | -spec terminate_all_children(Sup :: atom(), [pid()]) -> ok. 54 | terminate_all_children(_Sup, []) -> 55 | ok; 56 | terminate_all_children(Sup, [Pid|Rest]) -> 57 | ok = supervisor:terminate_child(Sup, Pid), 58 | terminate_all_children(Sup, Rest). 59 | -------------------------------------------------------------------------------- /src/ldclient_yaml_mapper.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc Utility to convert yamerl output into a map. 3 | %% @private 4 | %% @end 5 | %%------------------------------------------------------------------- 6 | 7 | -module(ldclient_yaml_mapper). 8 | 9 | %% API 10 | -export([to_map_docs/2]). 11 | 12 | -spec to_map_docs(DocsToMap :: list(), Results :: list()) -> 13 | list(). 14 | to_map_docs([Head | Rest], Results) -> 15 | Map = to_map(Head), 16 | to_map_docs(Rest, [Map | Results]); 17 | to_map_docs([], Results) -> 18 | Results. 19 | 20 | %%=================================================================== 21 | %% Internal functions 22 | %%=================================================================== 23 | 24 | to_map({yamerl_doc, Doc}) -> 25 | to_map(Doc); 26 | to_map({yamerl_seq, yamerl_node_seq, _Tag, _Loc, Seq, _N}) -> 27 | [to_map(X) || X <- Seq]; 28 | to_map({yamerl_map, yamerl_node_map, _Tag, _Loc, MapTuples}) -> 29 | tuples_to_map(MapTuples, #{}); 30 | to_map({yamerl_str, yamerl_node_str, _Tag, _Loc, CharList}) -> 31 | list_to_binary(CharList); 32 | to_map({yamerl_int, yamerl_node_int, _Tag, _Loc, Number}) -> 33 | Number; 34 | to_map({yamerl_bool, yamerl_node_bool, _Tag, _Loc, Bool}) -> 35 | Bool. 36 | 37 | tuples_to_map([], Map) -> 38 | Map; 39 | tuples_to_map([{Key, Val} | Rest], Map) -> 40 | {yamerl_str, yamerl_node_str, _, _, Name} = Key, 41 | tuples_to_map(Rest, maps:put(list_to_binary(Name), to_map(Val), Map)). 42 | -------------------------------------------------------------------------------- /test-redis/ldclient_stream_redis_online_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ldclient_stream_redis_online_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | 5 | %% ct functions 6 | -export([all/0]). 7 | -export([init_per_suite/1]). 8 | -export([end_per_suite/1]). 9 | -export([init_per_testcase/2]). 10 | -export([end_per_testcase/2]). 11 | 12 | %% Tests 13 | -export([ 14 | stream_sse_empty/1, 15 | stream_sse_simple_flag/1, 16 | stream_sse_put_no_path/1, 17 | stream_sse_timeout/1 18 | ]). 19 | 20 | %%==================================================================== 21 | %% ct functions 22 | %%==================================================================== 23 | 24 | all() -> 25 | [ 26 | stream_sse_empty, 27 | stream_sse_simple_flag, 28 | stream_sse_put_no_path, 29 | stream_sse_timeout 30 | ]. 31 | 32 | init_per_suite(Config) -> 33 | {ok, _} = application:ensure_all_started(http_server), 34 | {ok, _} = application:ensure_all_started(ldclient), 35 | Config. 36 | 37 | end_per_suite(_) -> 38 | ok = application:stop(ldclient), 39 | ok = application:stop(http_server), 40 | ok. 41 | 42 | init_per_testcase(_, Config) -> 43 | Config. 44 | 45 | end_per_testcase(_, _Config) -> 46 | ok. 47 | 48 | %%==================================================================== 49 | %% Helpers 50 | %%==================================================================== 51 | 52 | sdk_options(Prefix) -> 53 | #{ 54 | base_uri => "http://localhost:8888", 55 | stream_uri => "http://localhost:8888", 56 | events_uri => "http://localhost:8888", 57 | feature_store => ldclient_storage_redis, 58 | redis_prefix => Prefix 59 | }. 60 | 61 | %%==================================================================== 62 | %% Tests 63 | %%==================================================================== 64 | 65 | stream_sse_empty(_) -> 66 | ok = ldclient:start_instance("sdk-empty", sdk_options("prefixA")), 67 | % Wait for SDK to initialize and process initial payload from server 68 | timer:sleep(500), 69 | [] = ldclient_storage_redis:all(default, features), 70 | [] = ldclient_storage_redis:all(default, segments), 71 | ok = ldclient:stop_instance(), 72 | ok. 73 | 74 | stream_sse_simple_flag(_) -> 75 | ok = ldclient:start_instance("sdk-simple-flag", sdk_options("prefixB")), 76 | % Wait for SDK to initialize and process initial payload from server 77 | timer:sleep(500), 78 | {FlagSimpleKey, _FlagSimpleBin, FlagSimpleMap} = ldclient_test_utils:get_simple_flag(), 79 | ParsedFlagSimpleMap = ldclient_flag:new(FlagSimpleMap), 80 | [{FlagSimpleKey, ParsedFlagSimpleMap}] = ldclient_storage_redis:all(default, features), 81 | [] = ldclient_storage_redis:all(default, segments), 82 | ok = ldclient:stop_instance(), 83 | ok. 84 | 85 | stream_sse_put_no_path(_) -> 86 | ok = ldclient:start_instance("sdk-put-no-path", sdk_options("prefixC")), 87 | % Wait for SDK to initialize and process initial payload from server 88 | timer:sleep(500), 89 | {FlagSimpleKey, _FlagSimpleBin, FlagSimpleMap} = ldclient_test_utils:get_simple_flag(), 90 | ParsedFlagSimpleMap = ldclient_flag:new(FlagSimpleMap), 91 | [{FlagSimpleKey, ParsedFlagSimpleMap}] = ldclient_storage_redis:all(default, features), 92 | [] = ldclient_storage_redis:all(default, segments), 93 | ok = ldclient:stop_instance(), 94 | ok. 95 | 96 | stream_sse_timeout(_) -> 97 | ok = ldclient:start_instance("sdk-timeout", sdk_options("prefixD")), 98 | 99 | % Evaluation before redis has been initialized. 100 | {null,foo,{error,client_not_ready}} = ldclient:variation_detail(<<"abc">>, #{key => <<"123">>}, foo), 101 | #{flag_values := #{}} = ldclient:all_flags_state(#{key => <<"123">>}), 102 | #{<<"$flagsState">> := #{},<<"$valid">> := false} = ldclient:all_flags_state(#{key => <<"123">>}, #{}, default), 103 | 104 | % Wait for SDK to initialize and process initial payload from server 105 | timer:sleep(6500), 106 | {FlagSimpleKey, _FlagSimpleBin, FlagSimpleMap} = ldclient_test_utils:get_simple_flag(), 107 | ParsedFlagSimpleMap = ldclient_flag:new(FlagSimpleMap), 108 | [{FlagSimpleKey, ParsedFlagSimpleMap}] = ldclient_storage_redis:all(default, features), 109 | [] = ldclient_storage_redis:all(default, segments), 110 | % Evaluation after SDK is initialized should return an expected flag variation value 111 | {0, true, fallthrough} = ldclient:variation_detail(<<"abc">>, #{key => <<"123">>}, foo), 112 | 113 | % Start a second instance that uses the already initialized redis DB (PrefixD$inited is present). 114 | ok = ldclient:start_instance("sdk-timeout", with_init_redis, sdk_options("prefixD")), 115 | % Evaluation before SDK is initialized, but after the redis store has been initialized, should return 116 | % the cached values. 117 | {0,true,fallthrough} = ldclient:variation_detail(<<"abc">>, #{key => <<"123">>}, foo, with_init_redis), 118 | #{flag_values := #{<<"abc">> := true}} = ldclient:all_flags_state(#{key => <<"123">>}, with_init_redis), 119 | #{<<"$flagsState">> := 120 | #{<<"abc">> := 121 | #{<<"reason">> := #{kind := <<"FALLTHROUGH">>}, 122 | <<"trackEvents">> := true,<<"variation">> := 0, 123 | <<"version">> := 5}}, 124 | <<"$valid">> := true,<<"abc">> := true} = ldclient:all_flags_state(#{key => <<"123">>}, #{with_reasons => true}, with_init_redis), 125 | 126 | % Client should not be initialized yet. 127 | false = ldclient:initialized(with_init_redis), 128 | 129 | ok = ldclient:stop_instance(), 130 | ok = ldclient:stop_instance(with_init_redis), 131 | ok. 132 | -------------------------------------------------------------------------------- /test-service/README.md: -------------------------------------------------------------------------------- 1 | # SDK contract test service 2 | 3 | This directory contains an implementation of the cross-platform SDK testing protocol defined by https://github.com/launchdarkly/sdk-test-harness. See that project's `README` for details of this protocol, and the kinds of SDK capabilities that are relevant to the contract tests. This code should not need to be updated unless the SDK has added or removed such capabilities. 4 | 5 | To run these tests locally, run `make contract-tests` from the SDK project root directory. This downloads the correct version of the test harness tool automatically. 6 | 7 | Or, to test against an in-progress local version of the test harness, run `make start-contract-test-service` from the SDK project root directory; then, in the root directory of the `sdk-test-harness` project, build the test harness and run it from the command line. 8 | -------------------------------------------------------------------------------- /test-service/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | {deps, [ 3 | {shotgun, "1.0.1"}, 4 | {jsx, "3.1.0"}, 5 | {verl, "1.0.1"}, 6 | {lru, "2.4.0"}, 7 | {backoff, "1.1.6"}, 8 | {uuid, "2.0.2", {pkg, uuid_erl}}, 9 | {eredis, "1.7.1"}, 10 | {yamerl, "0.10.0"}, 11 | {certifi, "2.12.0"}, 12 | {cowboy, "2.8.0"}, 13 | ldclient 14 | ]}. 15 | 16 | {shell, [ 17 | {apps, [ldclient, ts_app]} 18 | ]}. 19 | 20 | {profiles, [ 21 | {prod, [{relx, [{dev_mode, false} 22 | ,{include_erts, true}]}]}, 23 | {otp26, 24 | [ 25 | {dialyzer, [ 26 | {plt_apps, top_level_deps}, 27 | %% The dialyzer is not accounting for checkouts. 28 | %% So for now we need to disable this. 29 | {warnings, [no_unknown]} 30 | ]} 31 | ]} 32 | ]}. 33 | 34 | {ct_opts, [{ct_hooks, [cth_surefire]}]}. 35 | 36 | {relx, [{release, {ts, "1.0.0"}, 37 | [ldclient, ts_app]}, 38 | 39 | {dev_mode, true}, 40 | {include_erts, false}, 41 | 42 | {extended_start_script, true}]}. 43 | 44 | -------------------------------------------------------------------------------- /test-service/src/ts_app.app.src: -------------------------------------------------------------------------------- 1 | {application, ts_app, 2 | [{description, "An OTP application"}, 3 | {vsn, "0.1.0"}, 4 | {mod, {ts_server, []}}, 5 | {applications, 6 | [kernel, 7 | stdlib, 8 | cowboy 9 | ]}, 10 | {env,[]}, 11 | {modules, []}, 12 | {start_phases, [{start_cowboy_http, []}]} 13 | ]}. 14 | -------------------------------------------------------------------------------- /test-service/src/ts_client_handler.erl: -------------------------------------------------------------------------------- 1 | %------------------------------------------------------------------- 2 | %% @doc `ts_client_handler' module 3 | %% 4 | %% Handle requests for creating and destroying clients. 5 | %% @private 6 | %% @end 7 | %%------------------------------------------------------------------- 8 | 9 | -module(ts_client_handler). 10 | -author("rlamb"). 11 | 12 | %% API 13 | -export([create_client/2]). 14 | -export([delete_client/1]). 15 | 16 | -spec wait_init(Tag :: atom(), Initialized :: boolean(), 17 | StartedWaiting :: pos_integer(), WaitTime :: pos_integer()) -> ok. 18 | wait_init(Tag, false, StartedWaiting, undefined) -> 19 | %% Default to 5 seconds. 20 | wait_init(Tag, false, StartedWaiting, 5000); 21 | wait_init(Tag, false, StartedWaiting, WaitTime) -> 22 | ElapsedTime = erlang:system_time(milli_seconds) - StartedWaiting, 23 | wait_init(Tag, ldclient:initialized(Tag) or (ElapsedTime >= WaitTime), StartedWaiting, WaitTime); 24 | wait_init(_Tag, true, _StartedWaiting, _WaitTime) -> ok. 25 | 26 | -spec create_client(Tag :: atom(), 27 | Params :: ts_service_params:create_instance_params()) -> ok. 28 | create_client(Tag, 29 | #{configuration := #{credential := Credential, 30 | start_wait_time_ms := StartWaitTimeMs} = Configuration} = _Params) -> 31 | Options = ts_sdk_config_params:to_ldclient_options(Configuration), 32 | ok = ldclient:start_instance(binary_to_list(Credential), Tag, Options), 33 | wait_init(Tag, ldclient:initialized(Tag), erlang:system_time(milli_seconds), StartWaitTimeMs), 34 | ok. 35 | 36 | -spec delete_client(Tag :: atom()) -> ok. 37 | delete_client(Tag) -> 38 | ldclient:stop_instance(Tag), 39 | ok. 40 | -------------------------------------------------------------------------------- /test-service/src/ts_command_handler.erl: -------------------------------------------------------------------------------- 1 | %------------------------------------------------------------------- 2 | %% @doc `ts_command_handler' module 3 | %% 4 | %% Handle commands which are directed to client instances. 5 | %% @private 6 | %% @end 7 | %%------------------------------------------------------------------- 8 | 9 | -module(ts_command_handler). 10 | 11 | -export([handle_command/2]). 12 | 13 | -spec handle_command(Tag :: atom(), Command :: ts_command_params:command_params()) -> error | map(). 14 | handle_command(Tag, #{command := evaluate, evaluate := Evaluate} = _Command) -> 15 | handle_evaluate_command(Tag, Evaluate); 16 | handle_command(Tag, #{command := flush_events} = _Command) -> 17 | handle_flush_events_command(Tag); 18 | handle_command(Tag, #{command := identify_event, identify_event := IdentifyEvent} = _Command) -> 19 | handle_identify_event_command(Tag, IdentifyEvent); 20 | handle_command(Tag, #{command := custom_event, custom_event := CustomEvent} = _Command) -> 21 | handle_custom_event_command(Tag, CustomEvent); 22 | handle_command(Tag, #{command := evaluate_all, evaluate_all := EvaluateAll} = _Command) -> 23 | handle_evaluate_all_command(Tag, EvaluateAll); 24 | handle_command(_Tag, _Command) -> 25 | error. 26 | 27 | -spec handle_evaluate_command(Tag :: atom(), 28 | Evaluate :: ts_command_params:evaluate_flag_params()) -> ts_command_params:evaluate_flag_response(). 29 | handle_evaluate_command(Tag, #{ 30 | flag_key := FlagKey, 31 | value_type := _ValueType, 32 | default_value := DefaultValue, 33 | detail := true} = Evaluate) -> 34 | ts_command_params:format_evaluate_flag_response( 35 | ldclient:variation_detail(FlagKey, user_or_context(Evaluate), DefaultValue, Tag)); 36 | handle_evaluate_command(Tag, #{ 37 | flag_key := FlagKey, 38 | value_type := _ValueType, 39 | default_value := DefaultValue} = Evaluate) -> 40 | ts_command_params:format_evaluate_flag_response( 41 | ldclient:variation(FlagKey, user_or_context(Evaluate), DefaultValue, Tag)). 42 | 43 | -spec handle_evaluate_all_command(Tag :: atom(), 44 | EvaluateAll :: ts_command_params:evaluate_all_flags_params()) 45 | -> ts_command_params:evaluate_all_flags_response(). 46 | handle_evaluate_all_command(Tag, EvaluateAll) -> 47 | #{state => ldclient:all_flags_state(user_or_context(EvaluateAll), EvaluateAll, Tag)}. 48 | 49 | -spec handle_flush_events_command(Tag :: atom()) -> ok. 50 | handle_flush_events_command(Tag) -> 51 | ldclient_event_server:flush(Tag). 52 | 53 | -spec handle_identify_event_command(Tag :: atom(), 54 | IdentifyEvent :: ts_command_params:identify_event_params()) -> ok. 55 | handle_identify_event_command(Tag, IdentifyEvent) -> 56 | ldclient:identify(user_or_context(IdentifyEvent), Tag). 57 | 58 | -spec handle_custom_event_command(Tag :: atom(), 59 | CustomEvent :: ts_command_params:custom_event_params()) -> ok. 60 | handle_custom_event_command(Tag, 61 | #{event_key := EventKey, omit_null_data := false, 62 | data := Data, metric_value := MetricValue} = CustomEvent) -> 63 | ldclient:track_metric(EventKey, user_or_context(CustomEvent), Data, MetricValue, Tag); 64 | handle_custom_event_command(Tag, 65 | #{event_key := EventKey, omit_null_data := false, data := Data} = CustomEvent) -> 66 | ldclient:track(EventKey, user_or_context(CustomEvent), Data, Tag); 67 | handle_custom_event_command(Tag, 68 | #{event_key := EventKey, omit_null_data := true, data := null, metric_value := MetricValue} = CustomEvent) -> 69 | ldclient:track_metric(EventKey, user_or_context(CustomEvent), null, MetricValue, Tag); 70 | handle_custom_event_command(Tag, 71 | #{event_key := EventKey, omit_null_data := true, data := null} = CustomEvent) -> 72 | ldclient:track(EventKey, user_or_context(CustomEvent), null, Tag); 73 | handle_custom_event_command(_Tag, _CustomEvent) -> 74 | % This combination doesn't seem valid, but this can be extended if it is. 75 | % No tests trigger this condition currently. 76 | ok. 77 | 78 | user_or_context(#{context := Context}) -> Context; 79 | user_or_context(#{user := User}) -> User. 80 | -------------------------------------------------------------------------------- /test-service/src/ts_command_request_handler.erl: -------------------------------------------------------------------------------- 1 | %------------------------------------------------------------------- 2 | %% @doc `ts_command_request_handler' module 3 | %% 4 | %% Handles requests from the test harness. 5 | %% @private 6 | %% @end 7 | %%------------------------------------------------------------------- 8 | 9 | -module(ts_command_request_handler). 10 | 11 | %% Cowboy callbacks 12 | -export([ 13 | init/2, 14 | allowed_methods/2, 15 | content_types_accepted/2, 16 | delete_resource/2 17 | ]). 18 | 19 | %% 20 | %% Additional callbacks 21 | -export([ 22 | handle_command/2 23 | ]). 24 | 25 | -spec init(Request :: cowboy_req:req(), State :: map()) -> {cowboy_rest, cowboy_req:req(), map()}. 26 | init(Req, Opts) -> 27 | {cowboy_rest, Req, Opts}. 28 | 29 | -spec allowed_methods(Request :: cowboy_req:req(), State :: map()) -> {[binary()], cowboy_req:req(), map()}. 30 | allowed_methods(Req, State) -> 31 | Methods = [<<"POST">>, <<"DELETE">>], 32 | {Methods, Req, State}. 33 | 34 | -spec content_types_accepted(Request :: cowboy_req:req(), State :: map()) -> {list(), cowboy_req:req(), map()}. 35 | content_types_accepted(Req, State) -> 36 | {[ 37 | {<<"application/json">>, handle_command} 38 | ], Req, State}. 39 | 40 | -spec delete_resource(Request :: cowboy_req:req(), State :: map()) -> {atom(), cowboy_req:req(), map()}. 41 | delete_resource(Req, State) -> 42 | ClientTag = cowboy_req:binding(client_tag, Req), 43 | ok = ts_client_handler:delete_client(binary_to_atom(ClientTag, utf8)), 44 | Req0 = cowboy_req:reply(204, Req), 45 | {true, Req0, State}. 46 | 47 | -spec handle_command(Request :: cowboy_req:req(), State :: map()) -> {atom(), cowboy_req:req(), map()}. 48 | handle_command(Req, State) -> 49 | ClientTag = cowboy_req:binding(client_tag, Req), 50 | {ok, Data, Req0} = cowboy_req:read_body(Req), 51 | DecodedJson = jsx:decode(Data, [return_maps]), 52 | Command = ts_command_params:parse_command(DecodedJson), 53 | ResponseBody = ts_command_handler:handle_command(binary_to_atom(ClientTag, utf8), Command), 54 | Req1 = cowboy_req:set_resp_body(jsx:encode(ResponseBody), Req0), 55 | {true, Req1, State}. -------------------------------------------------------------------------------- /test-service/src/ts_server.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ts_server' module 3 | %% 4 | %% Server application for contract test requests. 5 | %% @private 6 | %% @end 7 | %%------------------------------------------------------------------- 8 | 9 | -module(ts_server). 10 | 11 | -export([start/2]). 12 | -export([stop/1]). 13 | -export([start_phase/3]). 14 | 15 | %% @private 16 | start(_StartType, _StartArgs) -> 17 | ts_server_sup:start_link(). 18 | 19 | %% @private 20 | stop(_State) -> 21 | ok = cowboy:stop_listener(server). 22 | 23 | -spec start_phase(atom(), application:start_type(), []) -> ok | {error, term()}. 24 | start_phase(start_cowboy_http, _StartType, []) -> 25 | Port = application:get_env(ts_server, ts_port, 8000), 26 | Routes = [ 27 | { 28 | '_', 29 | [ 30 | {"/", ts_service_request_handler, #{}}, 31 | {"/client/:client_tag", ts_command_request_handler, #{}} 32 | ] 33 | } 34 | ], 35 | Dispatch = cowboy_router:compile(Routes), 36 | TransportOptions = [{port, Port}], 37 | ProtocolOptions = #{env => #{dispatch => Dispatch}}, 38 | {ok, _} = 39 | cowboy:start_clear(ts_server, TransportOptions, ProtocolOptions), 40 | ok. 41 | -------------------------------------------------------------------------------- /test-service/src/ts_server_sup.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @doc `ts_server_sup' module 3 | %% 4 | %% Supervisor for the contract testing server. 5 | %% @private 6 | %% @end 7 | %%------------------------------------------------------------------- 8 | 9 | -module(ts_server_sup). 10 | 11 | -behaviour(supervisor). 12 | 13 | -export([start_link/0]). 14 | -export([init/1]). 15 | 16 | %% admin api 17 | start_link() -> 18 | supervisor:start_link({local, ?MODULE}, ?MODULE, {}). 19 | 20 | %% behaviour callbacks 21 | init({}) -> 22 | {ok, {{one_for_one, 5, 10}, []} }. 23 | -------------------------------------------------------------------------------- /test-service/src/ts_service_params.erl: -------------------------------------------------------------------------------- 1 | %------------------------------------------------------------------- 2 | %% @doc `ts_sdk_config_params' module 3 | %% 4 | %% Parsers and types for service parameters. 5 | %% @private 6 | %% @end 7 | %%------------------------------------------------------------------- 8 | 9 | -module(ts_service_params). 10 | 11 | -export([ 12 | parse_create_instance_params/1 13 | ]). 14 | 15 | -type create_instance_params() :: #{ 16 | configuration => ts_sdk_config_params:sdk_config_params(), 17 | tag => string() 18 | }. 19 | 20 | -export_type([create_instance_params/0]). 21 | 22 | -spec parse_create_instance_params(Params :: map()) -> create_instance_params(). 23 | parse_create_instance_params(#{<<"configuration">> := ConfigurationMap} = Params) -> 24 | Configuration = ts_sdk_config_params:parse_config_params(ConfigurationMap), 25 | Tag = maps:get(<<"tag">>, Params, undefined), 26 | #{ 27 | configuration => Configuration, 28 | tag => Tag 29 | }. 30 | -------------------------------------------------------------------------------- /test-service/src/ts_service_request_handler.erl: -------------------------------------------------------------------------------- 1 | %------------------------------------------------------------------- 2 | %% @doc `ts_service_request_handler' module 3 | %% 4 | %% Handles root level request from the test harness. 5 | %% @private 6 | %% @end 7 | %%------------------------------------------------------------------- 8 | 9 | -module(ts_service_request_handler). 10 | 11 | %% Cowboy callbacks 12 | -export([ 13 | init/2, 14 | allowed_methods/2, 15 | content_types_provided/2, 16 | content_types_accepted/2, 17 | delete_resource/2 18 | ]). 19 | 20 | %% 21 | %% Additional callbacks 22 | -export([ 23 | get_service_detail/2, 24 | create_client/2 25 | ]). 26 | 27 | -spec init(Request :: cowboy_req:req(), State :: map()) -> {cowboy_rest, cowboy_req:req(), map()}. 28 | init(Req, Opt) -> 29 | {cowboy_rest, Req, Opt}. 30 | 31 | -spec allowed_methods(Request :: cowboy_req:req(), State :: map()) -> {[binary()], cowboy_req:req(), map()}. 32 | allowed_methods(Req, State) -> 33 | Methods = [<<"GET">>, <<"POST">>, <<"DELETE">>], 34 | {Methods, Req, State}. 35 | 36 | -spec content_types_provided(Request :: cowboy_req:req(), State :: map()) -> {list(), cowboy_req:req(), map()}. 37 | content_types_provided(Req, State) -> 38 | {[ 39 | {<<"application/json">>, get_service_detail} 40 | ], Req, State}. 41 | 42 | -spec content_types_accepted(Request :: cowboy_req:req(), State :: map()) -> {list(), cowboy_req:req(), map()}. 43 | content_types_accepted(Req, State) -> 44 | {[ 45 | {<<"application/json">>, create_client} 46 | ], Req, State}. 47 | 48 | -spec delete_resource(Request :: cowboy_req:req(), State :: map()) -> {atom(), cowboy_req:req(), map()}. 49 | delete_resource(Req, State) -> 50 | init:stop("Exit of service requested."), 51 | {true, Req, State}. 52 | 53 | -spec create_client(Request :: cowboy_req:req(), State :: map()) -> {true, cowboy_req:req(), map()}. 54 | create_client(Req, State) -> 55 | {ok, Data, Req0} = cowboy_req:read_body(Req), 56 | DecodedJson = jsx:decode(Data, [return_maps]), 57 | ParsedParams = ts_service_params:parse_create_instance_params(DecodedJson), 58 | {BinaryTag, Tag} = tag_from_integer(erlang:unique_integer([positive, monotonic])), 59 | ok = ts_client_handler:create_client(Tag, ParsedParams), 60 | ClientPath = <<"/client/">>, 61 | Headers = #{ 62 | <<"Location">> => <> 63 | }, 64 | Req1 = cowboy_req:set_resp_headers(Headers, Req0), 65 | {true, Req1, State}. 66 | 67 | -spec get_service_detail(Req :: cowboy_req:req(), State :: map()) -> {binary(), cowboy_req:req(), map()}. 68 | get_service_detail(Req, State) -> 69 | Body = jsx:encode(#{ 70 | <<"name">> => <<"erlang-server-sdk">>, 71 | <<"capabilities">> => [ 72 | <<"server-side">>, 73 | <<"all-flags-with-reasons">>, 74 | <<"tags">>, 75 | <<"server-side-polling">>, 76 | <<"user-type">>, 77 | <<"inline-context-all">>, 78 | <<"anonymous-redaction">>, 79 | <<"tls:custom-ca">>, 80 | <<"tls:skip-verify-peer">>, 81 | <<"tls:verify-peer">>, 82 | <<"client-prereq-events">>, 83 | <<"all-flags-client-side-only">> 84 | ], 85 | <<"clientVersion">> => ldclient_config:get_version() 86 | }), 87 | {Body, Req, State}. 88 | 89 | -spec tag_from_integer(Int :: number()) -> {binary(), atom()}. 90 | tag_from_integer(Int) -> 91 | BinaryInt = integer_to_binary(Int), 92 | Label = <<"c">>, 93 | BinaryTag = <