From 9459d4e3f65f6e63b3442fa376ae7956be6387a8 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 13 Mar 2025 15:51:44 -0500 Subject: [PATCH 1/6] wip --- .github/workflows/java-server-sdk-redis.yml | 68 +++++ .release-please-manifest.json | 1 + README.md | 12 +- lib/java-server-sdk-redis/CHANGELOG.md | 33 +++ lib/java-server-sdk-redis/README.md | 75 ++++++ lib/java-server-sdk-redis/build.gradle | 157 ++++++++++++ lib/java-server-sdk-redis/checkstyle.xml | 14 ++ lib/java-server-sdk-redis/gradle.properties | 6 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + lib/java-server-sdk-redis/gradlew | 234 ++++++++++++++++++ lib/java-server-sdk-redis/gradlew.bat | 89 +++++++ lib/java-server-sdk-redis/settings.gradle | 1 + .../sdk/server/integrations/Redis.java | 91 +++++++ .../RedisBigSegmentStoreImpl.java | 42 ++++ .../integrations/RedisDataStoreImpl.java | 163 ++++++++++++ .../integrations/RedisStoreBuilder.java | 205 +++++++++++++++ .../integrations/RedisStoreImplBase.java | 59 +++++ .../integrations/RedisURIComponents.java | 26 ++ .../RedisBigSegmentStoreImplTest.java | 51 ++++ .../RedisDataStoreBuilderTest.java | 81 ++++++ .../integrations/RedisDataStoreImplTest.java | 38 +++ .../integrations/RedisURIComponentsTest.java | 45 ++++ lib/sdk/server/build.gradle | 11 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../com/launchdarkly/sdk/server/Version.java | 2 - release-please-config.json | 8 + 27 files changed, 1510 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/java-server-sdk-redis.yml create mode 100644 lib/java-server-sdk-redis/CHANGELOG.md create mode 100644 lib/java-server-sdk-redis/README.md create mode 100644 lib/java-server-sdk-redis/build.gradle create mode 100644 lib/java-server-sdk-redis/checkstyle.xml create mode 100644 lib/java-server-sdk-redis/gradle.properties create mode 100644 lib/java-server-sdk-redis/gradle/wrapper/gradle-wrapper.jar create mode 100644 lib/java-server-sdk-redis/gradle/wrapper/gradle-wrapper.properties create mode 100755 lib/java-server-sdk-redis/gradlew create mode 100644 lib/java-server-sdk-redis/gradlew.bat create mode 100644 lib/java-server-sdk-redis/settings.gradle create mode 100644 lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/Redis.java create mode 100644 lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImpl.java create mode 100644 lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImpl.java create mode 100644 lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java create mode 100644 lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java create mode 100644 lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java create mode 100644 lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImplTest.java create mode 100644 lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreBuilderTest.java create mode 100644 lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImplTest.java create mode 100644 lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisURIComponentsTest.java diff --git a/.github/workflows/java-server-sdk-redis.yml b/.github/workflows/java-server-sdk-redis.yml new file mode 100644 index 0000000..db598e7 --- /dev/null +++ b/.github/workflows/java-server-sdk-redis.yml @@ -0,0 +1,68 @@ +name: java-server-sdk-redis + +on: + push: + branches: [main, 'feat/**'] + paths-ignore: + - '**.md' #Do not need to run CI for markdown changes. + pull_request: + branches: [main, 'feat/**'] + paths-ignore: + - '**.md' + +jobs: + # build-test-java-server-sdk-redis: + # strategy: + # matrix: + # jedis-version: [2.9.0, 3.0.0] + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + + # - run: | + # sudo apt-get update -y + # sudo apt-get install redis-server -y + # sudo service redis-server start + + # - name: Edit build.gradle to change Jedis version + # shell: bash + # run: | + # cd lib/java-server-sdk-redis + # sed -i.bak 's#"jedis":.*"[0-9.]*"#"jedis":"${{ matrix.jedis-version }}"#' build.gradle + + # - name: Shared CI Steps + # uses: ./.github/actions/ci + # with: + # workspace_path: 'lib/java-server-sdk-redis' + # java_version: 8 + + build-test-java-server-sdk-windows: + strategy: + matrix: + jedis-version: [2.9.0, 3.0.0] + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + + - run: | + $ProgressPreference = "SilentlyContinue" + iwr -outf redis.zip https://github.com/MicrosoftArchive/redis/releases/download/win-3.0.504/Redis-x64-3.0.504.zip + mkdir redis + Expand-Archive -Path redis.zip -DestinationPath redis + cd redis + .\redis-server --service-install + .\redis-server --service-start + Start-Sleep -s 5 + .\redis-cli ping + + - name: Edit build.gradle to change Jedis version + shell: bash + run: | + cd lib/java-server-sdk-redis + sed -i.bak 's#"jedis":.*"[0-9.]*"#"jedis":"${{ matrix.jedis-version }}"#' build.gradle + + - name: Shared CI Steps + uses: ./.github/actions/ci + with: + workspace_path: 'lib/java-server-sdk-redis' + java_version: 8 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 12b5b21..995d19c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,6 @@ { "lib/java-server-sdk-otel": "0.1.0", + "lib/java-server-sdk-redis": "3.0.0", "lib/shared/common": "2.1.1", "lib/shared/internal": "1.3.0", "lib/sdk/server": "7.7.0" diff --git a/README.md b/README.md index d80f475..72d39f5 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,10 @@ This includes shared libraries, used by SDKs and other tools, as well as SDKs. | [@launchdarkly/java-sdk-internal](lib/shared/internal/README.md) | [![Documentation][sdk-internal-docs-badge]][sdk-internal-docs-link] | [![maven][sdk-internal-maven-badge]][sdk-internal-maven-link] | [Issues][sdk-internal-issues] | [![Actions Status][sdk-internal-ci-badge]][sdk-internal-ci-link] | | [@launchdarkly/java-sdk-common](lib/shared/common/README.md) | [![Documentation][sdk-common-docs-badge]][sdk-common-docs-link] | [![maven][sdk-common-maven-badge]][sdk-common-maven-link] | [Issues][sdk-common-issues] | [![Actions Status][sdk-common-ci-badge]][sdk-common-ci-link] | -| Telemetry Packages | API Docs | maven | issues | tests | +| Other Packages | API Docs | maven | issues | tests | | ---------------------------------------------------------------------------- |--------------------------------------------------------------| ---------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------- | | [@launchdarkly/java-server-sdk-otel](lib/java-server-sdk-otel/README.md) | [![Documentation][server-otel-docs-badge]][server-otel-docs-link] | [![maven][server-otel-maven-badge]][server-otel-maven-link] | [Issues][server-otel-issues] | [![Actions Status][server-otel-ci-badge]][server-otel-ci-link] | +| [@launchdarkly/java-server-sdk-redis](lib/java-server-sdk-redis/README.md) | [![Documentation][server-redis-docs-badge]][server-redis-docs-link] | [![maven][server-redis-maven-badge]][server-redis-maven-link] | [Issues][server-redis-issues] | [![Actions Status][server-redis-ci-badge]][server-redis-ci-link] | ## Organization @@ -71,6 +72,15 @@ We encourage pull requests and other contributions from the community. Check out [server-otel-docs-badge]: https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8 [server-otel-docs-link]: https://launchdarkly.github.io/java-core/lib/java-server-sdk-otel/ +[//]: # 'java-server-sdk-redis' +[server-redis-issues]: https://github.com/launchdarkly/java-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+java-server-sdk-redis%22+ +[server-redis-maven-badge]: https://img.shields.io/maven-central/v/com.launchdarkly/launchdarkly-java-server-sdk-redis +[server-redis-maven-link]: https://central.sonatype.com/artifact/com.launchdarkly/launchdarkly-java-server-sdk-redis +[server-redis-ci-badge]: https://github.com/launchdarkly/java-core/actions/workflows/java-server-sdk-redis.yml/badge.svg +[server-redis-ci-link]: https://github.com/launchdarkly/java-core/actions/workflows/java-server-sdk-redis.yml +[server-redis-docs-badge]: https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8 +[server-redis-docs-link]: https://launchdarkly.github.io/java-core/lib/java-server-sdk-redis/ + [//]: # 'java-sdk-internal' [sdk-internal-issues]: https://github.com/launchdarkly/java-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+java-sdk-internal%22+ [sdk-internal-maven-badge]: https://img.shields.io/maven-central/v/com.launchdarkly/launchdarkly-java-sdk-internal diff --git a/lib/java-server-sdk-redis/CHANGELOG.md b/lib/java-server-sdk-redis/CHANGELOG.md new file mode 100644 index 0000000..6bbc3f4 --- /dev/null +++ b/lib/java-server-sdk-redis/CHANGELOG.md @@ -0,0 +1,33 @@ +# Change log + +All notable changes to the LaunchDarkly Java SDK Redis integration will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). + +## [3.0.0] - 2022-12-07 +This release corresponds to the 6.0.0 release of the LaunchDarkly Java SDK. Any application code that is being updated to use the 6.0.0 SDK, and was using a 2.x version of `launchdarkly-java-server-sdk-redis`, should now use a 3.x version instead. + +There are no functional differences in the behavior of the Redis integration; the differences are only related to changes in the usage of interface types for configuration in the SDK. + +### Added: +- `Redis.bigSegmentStore()`, which creates a configuration builder for use with Big Segments. Previously, the `Redis.dataStore()` builder was used for both regular data stores and Big Segment stores. + +### Changed: +- The type `RedisDataStoreBuilder` has been removed, replaced by a generic type `RedisStoreBuilder`. Application code would not normally need to reference these types by name, but if necessary, use either `RedisStoreBuilder` or `RedisStoreBuilder` depending on whether you are configuring a regular data store or a Big Segment store. + +## [2.0.0] - 2022-07-29 +This release updates the package to use the new logging mechanism that was introduced in version 5.10.0 of the LaunchDarkly Java SDK, so that log output from the Redis integration is handled in whatever way was specified by the SDK's logging configuration. + +This version of the package will not work with SDK versions earlier than 5.10.0; that is the only reason for the 2.0.0 major version increment. The functionality of the package is otherwise unchanged, and there are no API changes. + +## [1.1.0] - 2022-01-28 +### Added: +- Added support for Big Segments. An Early Access Program for creating and syncing Big Segments from customer data platforms is available to enterprise customers. + +## [1.0.1] - 2021-08-06 +### Fixed: +- This integration now works with Jedis 3.x as well as Jedis 2.9.x. The default dependency is still 2.9.x, but an application can override this with a dependency on a 3.x version. (Thanks, [robotmlg](https://github.com/launchdarkly/java-server-sdk-redis/pull/3)!) + +## [1.0.0] - 2020-06-02 +Initial release, corresponding to the 5.0.0 release of [`launchdarkly-java-server-sdk`](https://github.com/launchdarkly/java-server-sdk). + +Prior to that release, the Redis integration was built into the main SDK library. For more information about changes in the SDK database integrations, see the [4.x to 5.0 migration guide](https://docs-stg.launchdarkly.com/252/sdk/server-side/java/migration-4-to-5/). + diff --git a/lib/java-server-sdk-redis/README.md b/lib/java-server-sdk-redis/README.md new file mode 100644 index 0000000..94a1dc8 --- /dev/null +++ b/lib/java-server-sdk-redis/README.md @@ -0,0 +1,75 @@ +# LaunchDarkly SDK for Java - Redis integration + +[![Circle CI](https://circleci.com/gh/launchdarkly/java-server-sdk-redis.svg?style=shield)](https://circleci.com/gh/launchdarkly/java-server-sdk-redis) +[![Javadocs](http://javadoc.io/badge/com.launchdarkly/launchdarkly-java-server-sdk-redis-store.svg)](http://javadoc.io/doc/com.launchdarkly/launchdarkly-java-server-sdk-redis-store) + +This library provides a Redis-backed persistence mechanism (feature store) for the [LaunchDarkly Java SDK](https://github.com/launchdarkly/java-server-sdk), replacing the default in-memory feature store. The Redis API implementation it uses is [Jedis](https://github.com/xetorthio/jedis). + +This version of the library requires at least version 6.0.0 of the LaunchDarkly Java SDK; for versions of the library to use with earlier SDK versions, see the changelog. The minimum Java version is 8. + +For more information, see also: [Using Redis as a persistent feature store](https://docs.launchdarkly.com/sdk/features/storing-data/redis#java). + +## Quick setup + +This assumes that you have already installed the LaunchDarkly Java SDK. + +1. Add this library to your project (substitute the latest version number for `XXX`): + + + com.launchdarkly + launchdarkly-java-server-sdk-redis-store + XXX + + +2. The Redis client library (Jedis) should be pulled in automatically if you do not specify a dependency for it. If you want to use a different version, you may add your own dependency: + + + redis.clients + jedis + 2.9.0 + + + This library is compatible with Jedis 2.x versions greater than or equal to 2.9.0, and also with Jedis 3.x. + +3. Import the LaunchDarkly package and the package for this library: + + import com.launchdarkly.sdk.server.*; + import com.launchdarkly.sdk.server.integrations.*; + +4. When configuring your SDK client, add the Redis data store as a `persistentDataStore`. You may specify any custom Redis options using the methods of `RedisDataStoreBuilder`. For instance, to customize the Redis URL: + + LDConfig config = new LDConfig.Builder() + .dataStore( + Components.persistentDataStore( + Redis.dataStore().url("redis://my-redis-host") + ) + ) + .build(); + +By default, the store will try to connect to a local Redis instance on port 6379. + +## Caching behavior + +The LaunchDarkly SDK has a standard caching mechanism for any persistent data store, to reduce database traffic. This is configured through the SDK's `PersistentDataStoreBuilder` class as described the SDK documentation. For instance, to specify a cache TTL of 5 minutes: + + LDConfig config = new LDConfig.Builder() + .dataStore( + Components.persistentDataStore( + Redis.dataStore() + ).cacheTime(Duration.ofMinutes(5)) + ) + .build(); + +## About LaunchDarkly + +* 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: + * 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. + * 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?). + * 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. + * 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. +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. +* Explore LaunchDarkly + * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information + * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides + * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation + * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates diff --git a/lib/java-server-sdk-redis/build.gradle b/lib/java-server-sdk-redis/build.gradle new file mode 100644 index 0000000..5fc3bae --- /dev/null +++ b/lib/java-server-sdk-redis/build.gradle @@ -0,0 +1,157 @@ + +buildscript { + repositories { + mavenCentral() + mavenLocal() + } +} + +plugins { + id "java" + id "java-library" + id "checkstyle" + id "signing" + id "maven-publish" + id "de.marcphilipp.nexus-publish" version "0.3.0" + id "io.codearte.nexus-staging" version "0.30.0" + id "idea" +} + +configurations.all { + // check for updates every build for dependencies with: 'changing: true' + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' +} + +repositories { + mavenLocal() + // Before LaunchDarkly release artifacts get synced to Maven Central they are here along with snapshots: + maven { url "https://oss.sonatype.org/content/groups/public/" } + mavenCentral() +} + +allprojects { + group = 'com.launchdarkly' + version = "${version}" + archivesBaseName = 'launchdarkly-java-server-sdk-redis-store' + sourceCompatibility = 1.8 + targetCompatibility = 1.8 +} + +ext { + sdkBasePackage = "com.launchdarkly.client.redis" +} + +ext.versions = [ + "sdk": "6.2.1", // the *lowest* version we're compatible with + "jedis": "2.9.0" +] + +ext.libraries = [:] + +dependencies { + api "com.launchdarkly:launchdarkly-java-server-sdk:${versions.sdk}" + api "redis.clients:jedis:${versions.jedis}" + testImplementation "org.hamcrest:hamcrest-all:1.3" + testImplementation "junit:junit:4.12" + testImplementation "com.launchdarkly:launchdarkly-java-server-sdk:${versions.sdk}:test" // our unit tests use helper classes from the SDK + testImplementation "com.google.guava:guava:28.2-jre" // required by SDK tests, not used in this library itself + testImplementation "com.google.code.gson:gson:2.7" // same as above +} + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar, javadocJar +} + +test { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + showStandardStreams = true + exceptionFormat = 'full' + } +} + +checkstyle { + toolVersion = "9.3" + configFile = file("${project.rootDir}/checkstyle.xml") +} + +idea { + module { + downloadJavadoc = true + downloadSources = true + } +} + + +nexusStaging { + packageGroup = "com.launchdarkly" + numberOfRetries = 40 // we've seen extremely long delays in closing repositories +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + + groupId = 'com.launchdarkly' + artifactId = project.archivesBaseName + + artifact sourcesJar + artifact javadocJar + + pom { + name = project.archivesBaseName + description = 'LaunchDarkly Java SDK Redis integration' + url = 'https://github.com/launchdarkly/java-server-sdk-redis' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + name = 'LaunchDarkly' + email = 'team@launchdarkly.com' + } + } + scm { + connection = 'scm:git:git://github.com/launchdarkly/java-server-sdk-redis.git' + developerConnection = 'scm:git:ssh:git@github.com:launchdarkly/java-server-sdk-redis.git' + url = 'https://github.com/launchdarkly/java-server-sdk-redis' + } + } + } + } + repositories { + mavenLocal() + } +} + +nexusPublishing { + clientTimeout = java.time.Duration.ofMinutes(2) // we've seen extremely long delays in creating repositories + repositories { + sonatype { + username = ossrhUsername + password = ossrhPassword + } + } +} + +signing { + sign publishing.publications.mavenJava +} + +tasks.withType(Sign) { + onlyIf { !"1".equals(project.findProperty("LD_SKIP_SIGNING")) } // so we can build jars for testing in CI +} diff --git a/lib/java-server-sdk-redis/checkstyle.xml b/lib/java-server-sdk-redis/checkstyle.xml new file mode 100644 index 0000000..0101956 --- /dev/null +++ b/lib/java-server-sdk-redis/checkstyle.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/java-server-sdk-redis/gradle.properties b/lib/java-server-sdk-redis/gradle.properties new file mode 100644 index 0000000..7e6a212 --- /dev/null +++ b/lib/java-server-sdk-redis/gradle.properties @@ -0,0 +1,6 @@ +version=3.0.0 +ossrhUsername= +ossrhPassword= + +# See https://github.com/gradle/gradle/issues/11308 regarding the following property +systemProp.org.gradle.internal.publish.checksums.insecure=true diff --git a/lib/java-server-sdk-redis/gradle/wrapper/gradle-wrapper.jar b/lib/java-server-sdk-redis/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/lib/java-server-sdk-redis/gradle/wrapper/gradle-wrapper.properties b/lib/java-server-sdk-redis/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..070cb70 --- /dev/null +++ b/lib/java-server-sdk-redis/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lib/java-server-sdk-redis/gradlew b/lib/java-server-sdk-redis/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/lib/java-server-sdk-redis/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/lib/java-server-sdk-redis/gradlew.bat b/lib/java-server-sdk-redis/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/lib/java-server-sdk-redis/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/java-server-sdk-redis/settings.gradle b/lib/java-server-sdk-redis/settings.gradle new file mode 100644 index 0000000..f76074f --- /dev/null +++ b/lib/java-server-sdk-redis/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'launchdarkly-java-server-sdk-redis-store' diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/Redis.java b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/Redis.java new file mode 100644 index 0000000..92eb78f --- /dev/null +++ b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/Redis.java @@ -0,0 +1,91 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.launchdarkly.sdk.server.Components; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; + +/** + * Integration between the LaunchDarkly SDK and Redis. + * + * @since 4.12.0 + */ +public abstract class Redis { + /** + * Returns a builder object for creating a Redis-backed persistent data store. + *

+ * This is for the main data store that holds feature flag data. To configure a + * Big Segment store, use {@link #bigSegmentStore()} instead. + *

+ * You can use methods of the builder to specify any non-default Redis options + * you may want, before passing the builder to {@link Components#persistentDataStore(ComponentConfigurer)}. + * In this example, the store is configured to use a Redis host called "host1": + *


+   *     LDConfig config = new LDConfig.Builder()
+   *         .dataStore(
+   *             Components.persistentDataStore(
+   *                 Redis.dataStore().uri(URI.create("redis://host1:6379")
+   *             )
+   *         )
+   *         .build();
+   * 
+ *

+ * Note that the SDK also has its own options related to data storage that are configured + * at a different level, because they are independent of what database is being used. For + * instance, the builder returned by {@link Components#persistentDataStore(ComponentConfigurer)} + * has options for caching: + *


+   *     LDConfig config = new LDConfig.Builder()
+   *         .dataStore(
+   *             Components.persistentDataStore(
+   *                 Redis.dataStore().uri(URI.create("redis://my-redis-host"))
+   *             ).cacheSeconds(15)
+   *         )
+   *         .build();
+   * 
+ * + * @return a data store configuration object + */ + public static RedisStoreBuilder dataStore() { + return new RedisStoreBuilder.ForDataStore(); + } + + /** + * Returns a builder object for creating a Redis-backed Big Segment store. + *

+ * You can use methods of the builder to specify any non-default Redis options + * you may want, before passing the builder to {@link Components#bigSegments(ComponentConfigurer)}. + * In this example, the store is configured to use a Redis host called "host2": + *


+   *     LDConfig config = new LDConfig.Builder()
+   *         .bigSegments(
+   *             Components.bigSegments(
+   *                 Redis.bigSegmentStore().uri(URI.create("redis://host2:6379")
+   *             )
+   *         )
+   *         .build();
+   * 
+ *

+ * Note that the SDK also has its own options related to Big Segments that are configured + * at a different level, because they are independent of what database is being used. For + * instance, the builder returned by {@link Components#bigSegments(ComponentConfigurer)} + * has an option for the status polling interval: + *


+   *     LDConfig config = new LDConfig.Builder()
+   *         .dataStore(
+   *             Components.bigSegments(
+   *                 Redis.bigSegmentStore().uri(URI.create("redis://my-redis-host"))
+   *             ).statusPollInterval(Duration.ofSeconds(30))
+   *         )
+   *         .build();
+   * 
+ * + * @return a Big Segment store configuration object + * @since 3.0.0 + */ + public static RedisStoreBuilder bigSegmentStore() { + return new RedisStoreBuilder.ForBigSegments(); + } + + private Redis() {} +} diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImpl.java b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImpl.java new file mode 100644 index 0000000..a287776 --- /dev/null +++ b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImpl.java @@ -0,0 +1,42 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes; + +import java.util.Set; + +import redis.clients.jedis.Jedis; + +final class RedisBigSegmentStoreImpl extends RedisStoreImplBase implements BigSegmentStore { + private final String syncTimeKey; + private final String includedKeyPrefix; + private final String excludedKeyPrefix; + + RedisBigSegmentStoreImpl(RedisStoreBuilder builder, LDLogger baseLogger) { + super(builder, baseLogger.subLogger("BigSegments").subLogger("Redis")); + syncTimeKey = prefix + ":big_segments_synchronized_on"; + includedKeyPrefix = prefix + ":big_segment_include:"; + excludedKeyPrefix = prefix + ":big_segment_exclude:"; + } + + @Override + public BigSegmentStoreTypes.Membership getMembership(String userHash) { + try (Jedis jedis = pool.getResource()) { + Set includedRefs = jedis.smembers(includedKeyPrefix + userHash); + Set excludedRefs = jedis.smembers(excludedKeyPrefix + userHash); + return BigSegmentStoreTypes.createMembershipFromSegmentRefs(includedRefs, excludedRefs); + } + } + + @Override + public BigSegmentStoreTypes.StoreMetadata getMetadata() { + try (Jedis jedis = pool.getResource()) { + String value = jedis.get(syncTimeKey); + if (value == null || value.isEmpty()) { + return null; + } + return new BigSegmentStoreTypes.StoreMetadata(Long.parseLong(value)); + } + } +} diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImpl.java b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImpl.java new file mode 100644 index 0000000..aebd26b --- /dev/null +++ b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImpl.java @@ -0,0 +1,163 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.SerializedItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.Transaction; + +final class RedisDataStoreImpl extends RedisStoreImplBase implements PersistentDataStore { + private UpdateListener updateListener; + + RedisDataStoreImpl(RedisStoreBuilder builder, LDLogger baseLogger) { + super(builder, baseLogger.subLogger("DataStore").subLogger("Redis")); + } + + @Override + public SerializedItemDescriptor get(DataKind kind, String key) { + try (Jedis jedis = pool.getResource()) { + String item = getRedis(kind, key, jedis); + return item == null ? null : new SerializedItemDescriptor(0, false, item); + } + } + + @Override + public KeyedItems getAll(DataKind kind) { + try (Jedis jedis = pool.getResource()) { + Map allJson = jedis.hgetAll(itemsKey(kind)); + List> itemsOut = new ArrayList<>(allJson.size()); + for (Map.Entry e: allJson.entrySet()) { + itemsOut.add(new AbstractMap.SimpleEntry<>(e.getKey(), new SerializedItemDescriptor(0, false, e.getValue()))); + } + return new KeyedItems<>(itemsOut); + } + } + + @Override + public void init(FullDataSet allData) { + try (Jedis jedis = pool.getResource()) { + Transaction t = jedis.multi(); + + for (Map.Entry> e0: allData.getData()) { + DataKind kind = e0.getKey(); + String baseKey = itemsKey(kind); + t.del(baseKey); + for (Map.Entry e1: e0.getValue().getItems()) { + t.hset(baseKey, e1.getKey(), jsonOrPlaceholder(kind, e1.getValue())); + } + } + + t.set(initedKey(), ""); + t.exec(); + } + } + + @Override + public boolean upsert(DataKind kind, String key, SerializedItemDescriptor newItem) { + while (true) { + Jedis jedis = null; + try { + jedis = pool.getResource(); + String baseKey = itemsKey(kind); + jedis.watch(baseKey); + + if (updateListener != null) { + updateListener.aboutToUpdate(baseKey, key); + } + + String oldItemJson = getRedis(kind, key, jedis); + // In this implementation, we have to parse the existing item in order to determine its version. + int oldVersion = oldItemJson == null ? -1 : kind.deserialize(oldItemJson).getVersion(); + + if (oldVersion >= newItem.getVersion()) { + logger.debug("Attempted to {} key: {} version: {}" + + " with a version that is the same or older: {} in \"{}\"", + newItem.getSerializedItem() == null ? "delete" : "update", + key, oldVersion, newItem.getVersion(), kind.getName()); + return false; + } + + Transaction tx = jedis.multi(); + tx.hset(baseKey, key, jsonOrPlaceholder(kind, newItem)); + List result = tx.exec(); + if (result == null || result.isEmpty()) { + // if exec failed, it means the watch was triggered and we should retry + logger.debug("Concurrent modification detected, retrying"); + continue; + } + + return true; + } finally { + if (jedis != null) { + jedis.unwatch(); + jedis.close(); + } + } + } + } + + @Override + public boolean isInitialized() { + try (Jedis jedis = pool.getResource()) { + return jedis.exists(initedKey()); + } + } + + @Override + public boolean isStoreAvailable() { + try { + isInitialized(); // don't care about the return value, just that it doesn't throw an exception + return true; + } catch (Exception e) { // don't care about exception class, since any exception means the Redis request couldn't be made + return false; + } + } + + // package-private for testing + void setUpdateListener(UpdateListener updateListener) { + this.updateListener = updateListener; + } + + private String itemsKey(DataKind kind) { + return prefix + ":" + kind.getName(); + } + + private String initedKey() { + return prefix + ":$inited"; + } + + private String getRedis(DataKind kind, String key, Jedis jedis) { + String json = jedis.hget(itemsKey(kind), key); + + if (json == null) { + logger.debug("[get] Key: {} not found in \"{}\". Returning null", key, kind.getName()); + } + + return json; + } + + private static String jsonOrPlaceholder(DataKind kind, SerializedItemDescriptor serializedItem) { + String s = serializedItem.getSerializedItem(); + if (s != null) { + return s; + } + // For backward compatibility with previous implementations of the Redis integration, we must store a + // special placeholder string for deleted items. DataKind.serializeItem() will give us this string if + // we pass a deleted ItemDescriptor. + return kind.serialize(ItemDescriptor.deletedItem(serializedItem.getVersion())); + } + + static interface UpdateListener { + void aboutToUpdate(String baseKey, String itemKey); + } +} diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java new file mode 100644 index 0000000..d8df2ec --- /dev/null +++ b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java @@ -0,0 +1,205 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.Components; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DiagnosticDescription; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; + +import java.net.URI; +import java.time.Duration; + +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.Protocol; + +/** + * A builder for configuring the + * Redis-based persistent data store and/or Big Segment store. + *

+ * Both {@link Redis#dataStore()} and {@link Redis#bigSegmentStore()} return instances of + * this class. You can use methods of the builder to specify any non-default Redis options + * you may want, before passing the builder to either {@link Components#persistentDataStore(ComponentConfigurer)} + * or {@link Components#bigSegments(ComponentConfigurer)} as appropriate. The two types of + * stores are independent of each other; you do not need a Big Segment store if you are not + * using the Big Segments feature, and you do not need to use the same database for both. + * + * In this example, the main data store uses a Redis host called "host1", and the Big Segment + * store uses a Redis host called "host2": + *


+ *     LDConfig config = new LDConfig.Builder()
+ *         .dataStore(
+ *             Components.persistentDataStore(
+ *                 Redis.dataStore().uri(URI.create("redis://host1:6379")
+ *             )
+ *         )
+ *         .bigSegments(
+ *             Components.bigSegments(
+ *                 Redis.dataStore().uri(URI.create("redis://host2:6379")
+ *             )
+ *         )
+ *         .build();
+ * 
+ *

+ * Note that the SDK also has its own options related to data storage that are configured + * at a different level, because they are independent of what database is being used. For + * instance, the builder returned by {@link Components#persistentDataStore(ComponentConfigurer)} + * has options for caching: + *


+ *     LDConfig config = new LDConfig.Builder()
+ *         .dataStore(
+ *             Components.persistentDataStore(
+ *                 Redis.dataStore().uri(URI.create("redis://my-redis-host"))
+ *             ).cacheSeconds(15)
+ *         )
+ *         .build();
+ * 
+ * + * @param the component type that this builder is being used for + * + * @since 5.0.0 + */ +public abstract class RedisStoreBuilder implements ComponentConfigurer, DiagnosticDescription { + /** + * The default value for the Redis URI: {@code redis://localhost:6379} + */ + public static final URI DEFAULT_URI = URI.create("redis://localhost:6379"); + + /** + * The default value for {@link #prefix(String)}. + */ + public static final String DEFAULT_PREFIX = "launchdarkly"; + + URI uri = DEFAULT_URI; + String prefix = DEFAULT_PREFIX; + Duration connectTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT); + Duration socketTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT); + Integer database = null; + String password = null; + boolean tls = false; + JedisPoolConfig poolConfig = null; + + // These constructors are called only from Implementations + RedisStoreBuilder() { + } + + /** + * Specifies the database number to use. + *

+ * The database number can also be specified in the Redis URI, in the form {@code redis://host:port/NUMBER}. Any + * non-null value that you set with {@link #database(Integer)} will override the URI. + * + * @param database the database number, or null to fall back to the URI or the default + * @return the builder + */ + public RedisStoreBuilder database(Integer database) { + this.database = database; + return this; + } + + /** + * Specifies a password that will be sent to Redis in an AUTH command. + *

+ * It is also possible to include a password in the Redis URI, in the form {@code redis://:PASSWORD@host:port}. Any + * password that you set with {@link #password(String)} will override the URI. + * + * @param password the password + * @return the builder + */ + public RedisStoreBuilder password(String password) { + this.password = password; + return this; + } + + /** + * Optionally enables TLS for secure connections to Redis. + *

+ * This is equivalent to specifying a Redis URI that begins with {@code rediss:} rather than {@code redis:}. + *

+ * Note that not all Redis server distributions support TLS. + * + * @param tls true to enable TLS + * @return the builder + */ + public RedisStoreBuilder tls(boolean tls) { + this.tls = tls; + return this; + } + + /** + * Specifies a Redis host URI other than {@link #DEFAULT_URI}. + * + * @param redisUri the URI of the Redis host + * @return the builder + */ + public RedisStoreBuilder uri(URI redisUri) { + this.uri = redisUri; + return this; + } + + /** + * Optionally configures the namespace prefix for all keys stored in Redis. + * + * @param prefix the namespace prefix + * @return the builder + */ + public RedisStoreBuilder prefix(String prefix) { + this.prefix = prefix; + return this; + } + + /** + * Optional override if you wish to specify your own configuration to the underlying Jedis pool. + * + * @param poolConfig the Jedis pool configuration. + * @return the builder + */ + public RedisStoreBuilder poolConfig(JedisPoolConfig poolConfig) { + this.poolConfig = poolConfig; + return this; + } + + /** + * Optional override which sets the connection timeout for the underlying Jedis pool which otherwise defaults to + * {@link redis.clients.jedis.Protocol#DEFAULT_TIMEOUT} milliseconds. + * + * @param connectTimeout the timeout + * @return the builder + */ + public RedisStoreBuilder connectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout == null ? Duration.ofMillis(Protocol.DEFAULT_TIMEOUT) : connectTimeout; + return this; + } + + /** + * Optional override which sets the connection timeout for the underlying Jedis pool which otherwise defaults to + * {@link redis.clients.jedis.Protocol#DEFAULT_TIMEOUT} milliseconds. + * + * @param socketTimeout the socket timeout + * @return the builder + */ + public RedisStoreBuilder socketTimeout(Duration socketTimeout) { + this.socketTimeout = socketTimeout == null ? Duration.ofMillis(Protocol.DEFAULT_TIMEOUT) : socketTimeout; + return this; + } + + @Override + public LDValue describeConfiguration(ClientContext clientContext) { + return LDValue.of("Redis"); + } + + static final class ForDataStore extends RedisStoreBuilder { + @Override + public PersistentDataStore build(ClientContext clientContext) { + return new RedisDataStoreImpl(this, clientContext.getBaseLogger()); + } + } + + static final class ForBigSegments extends RedisStoreBuilder { + @Override + public BigSegmentStore build(ClientContext clientContext) { + return new RedisBigSegmentStoreImpl(this, clientContext.getBaseLogger()); + } + } +} diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java new file mode 100644 index 0000000..fa1ff5c --- /dev/null +++ b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java @@ -0,0 +1,59 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.launchdarkly.logging.LDLogger; + +import java.io.Closeable; +import java.io.IOException; + +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +abstract class RedisStoreImplBase implements Closeable { + protected final LDLogger logger; + protected final JedisPool pool; + protected final String prefix; + + protected RedisStoreImplBase(RedisStoreBuilder builder, LDLogger logger) { + this.logger = logger; + + // There is no builder for JedisPool, just a large number of constructor overloads. Unfortunately, + // the overloads that accept a URI do not accept the other parameters we need to set, so we need + // to decompose the URI. + String host = builder.uri.getHost(); + int port = builder.uri.getPort(); + String password = builder.password == null ? RedisURIComponents.getPassword(builder.uri) : builder.password; + int database = builder.database == null ? RedisURIComponents.getDBIndex(builder.uri) : builder.database; + boolean tls = builder.tls || builder.uri.getScheme().equals("rediss"); + + String extra = tls ? " with TLS" : ""; + if (password != null) { + extra = extra + (extra.isEmpty() ? " with" : " and") + " password"; + } + logger.info("Using Redis data store at {}:{}/{}{}", host, port, database, extra); + + JedisPoolConfig poolConfig = (builder.poolConfig != null) ? builder.poolConfig : new JedisPoolConfig(); + + this.prefix = (builder.prefix == null || builder.prefix.isEmpty()) ? + RedisStoreBuilder.DEFAULT_PREFIX : + builder.prefix; + this.pool = new JedisPool(poolConfig, + host, + port, + (int) builder.connectTimeout.toMillis(), + (int) builder.socketTimeout.toMillis(), + password, + database, + null, // clientName + tls, + null, // sslSocketFactory + null, // sslParameters + null // hostnameVerifier + ); + } + + @Override + public void close() throws IOException { + logger.info("Closing Redis store"); + pool.destroy(); + } +} diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java new file mode 100644 index 0000000..3c39ccc --- /dev/null +++ b/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java @@ -0,0 +1,26 @@ +package com.launchdarkly.sdk.server.integrations; + +import java.net.URI; + +/** + * This class contains methods equivalent to those in JedisURIHelper. Avoiding the use of + * JedisURIHelper allows us to be compatible with both Jedis 2.x and Jedis 3.x, because + * that class doesn't exist in the same location in both versions. + */ +abstract class RedisURIComponents { + static String getPassword(URI uri) { + if (uri.getUserInfo() == null) { + return null; + } + String[] parts = uri.getUserInfo().split(":", 2); + return parts.length < 2 ? null : parts[1]; + } + + static int getDBIndex(URI uri) { + String[] parts = uri.getPath().split("/", 2); + if (parts.length < 2 || parts[1].isEmpty()) { + return 0; + } + return Integer.parseInt(parts[1]); + } +} diff --git a/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImplTest.java b/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImplTest.java new file mode 100644 index 0000000..0ece4be --- /dev/null +++ b/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImplTest.java @@ -0,0 +1,51 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; + +import redis.clients.jedis.Jedis; + +@SuppressWarnings("javadoc") +public class RedisBigSegmentStoreImplTest extends BigSegmentStoreTestBase { + + @Override + protected ComponentConfigurer makeStore(String prefix) { + return Redis.bigSegmentStore().prefix(prefix); + } + + @Override + protected void clearData(String prefix) { + prefix = prefix == null || prefix.isEmpty() ? RedisStoreBuilder.DEFAULT_PREFIX : prefix; + try (Jedis client = new Jedis("localhost")) { + for (String key : client.keys(prefix + ":*")) { + client.del(key); + } + } + } + + @Override + protected void setMetadata(String prefix, BigSegmentStoreTypes.StoreMetadata storeMetadata) { + try (Jedis client = new Jedis("localhost")) { + client.set(prefix + ":big_segments_synchronized_on", + storeMetadata != null ? Long.toString(storeMetadata.getLastUpToDate()) : ""); + } + } + + @Override + protected void setSegments(String prefix, + String userHashKey, + Iterable includedSegmentRefs, + Iterable excludedSegmentRefs) { + try (Jedis client = new Jedis("localhost")) { + String includeKey = prefix + ":big_segment_include:" + userHashKey; + String excludeKey = prefix + ":big_segment_exclude:" + userHashKey; + for (String includedSegmentRef : includedSegmentRefs) { + client.sadd(includeKey, includedSegmentRef); + } + for (String excludedSegmentRef : excludedSegmentRefs) { + client.sadd(excludeKey, excludedSegmentRef); + } + } + } +} diff --git a/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreBuilderTest.java b/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreBuilderTest.java new file mode 100644 index 0000000..45b840a --- /dev/null +++ b/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreBuilderTest.java @@ -0,0 +1,81 @@ +package com.launchdarkly.sdk.server.integrations; + +import org.junit.Test; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.Protocol; + +@SuppressWarnings("javadoc") +public class RedisDataStoreBuilderTest { + @Test + public void testDefaultValues() { + RedisStoreBuilder conf = Redis.dataStore(); + assertEquals(RedisStoreBuilder.DEFAULT_URI, conf.uri); + assertNull(conf.database); + assertNull(conf.password); + assertFalse(conf.tls); + assertEquals(Duration.ofMillis(Protocol.DEFAULT_TIMEOUT), conf.connectTimeout); + assertEquals(Duration.ofMillis(Protocol.DEFAULT_TIMEOUT), conf.socketTimeout); + assertEquals(RedisStoreBuilder.DEFAULT_PREFIX, conf.prefix); + assertNull(conf.poolConfig); + } + + @Test + public void testUriConfigured() { + URI uri = URI.create("redis://other:9999"); + RedisStoreBuilder conf = Redis.dataStore().uri(uri); + assertEquals(uri, conf.uri); + } + + @Test + public void testDatabaseConfigured() { + RedisStoreBuilder conf = Redis.dataStore().database(3); + assertEquals(Integer.valueOf(3), conf.database); + } + + @Test + public void testPasswordConfigured() { + RedisStoreBuilder conf = Redis.dataStore().password("secret"); + assertEquals("secret", conf.password); + } + + @Test + public void testTlsConfigured() { + RedisStoreBuilder conf = Redis.dataStore().tls(true); + assertTrue(conf.tls); + } + + @Test + public void testPrefixConfigured() throws URISyntaxException { + RedisStoreBuilder conf = Redis.dataStore().prefix("prefix"); + assertEquals("prefix", conf.prefix); + } + + @Test + public void testConnectTimeoutConfigured() throws URISyntaxException { + RedisStoreBuilder conf = Redis.dataStore().connectTimeout(Duration.ofSeconds(1)); + assertEquals(Duration.ofSeconds(1), conf.connectTimeout); + } + + @Test + public void testSocketTimeoutConfigured() throws URISyntaxException { + RedisStoreBuilder conf = Redis.dataStore().socketTimeout(Duration.ofSeconds(1)); + assertEquals(Duration.ofSeconds(1), conf.socketTimeout); + } + + @Test + public void testPoolConfigConfigured() throws URISyntaxException { + JedisPoolConfig poolConfig = new JedisPoolConfig(); + RedisStoreBuilder conf = Redis.dataStore().poolConfig(poolConfig); + assertEquals(poolConfig, conf.poolConfig); + } +} diff --git a/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImplTest.java b/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImplTest.java new file mode 100644 index 0000000..63e7f08 --- /dev/null +++ b/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImplTest.java @@ -0,0 +1,38 @@ +package com.launchdarkly.sdk.server.integrations; + +import com.launchdarkly.sdk.server.integrations.RedisDataStoreImpl.UpdateListener; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; + +import java.net.URI; + +import redis.clients.jedis.Jedis; + +@SuppressWarnings("javadoc") +public class RedisDataStoreImplTest extends PersistentDataStoreTestBase { + + private static final URI REDIS_URI = URI.create("redis://localhost:6379"); + + @Override + protected ComponentConfigurer buildStore(String prefix) { + return Redis.dataStore().uri(REDIS_URI).prefix(prefix); + } + + @Override + protected void clearAllData() { + try (Jedis client = new Jedis("localhost")) { + client.flushDB(); + } + } + + @Override + protected boolean setUpdateHook(RedisDataStoreImpl storeUnderTest, final Runnable hook) { + storeUnderTest.setUpdateListener(new UpdateListener() { + @Override + public void aboutToUpdate(String baseKey, String itemKey) { + hook.run(); + } + }); + return true; + } +} diff --git a/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisURIComponentsTest.java b/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisURIComponentsTest.java new file mode 100644 index 0000000..daaca7f --- /dev/null +++ b/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisURIComponentsTest.java @@ -0,0 +1,45 @@ +package com.launchdarkly.sdk.server.integrations; + +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class RedisURIComponentsTest { + @Test + public void getPasswordForURIWithoutUserInfo() { + assertNull(RedisURIComponents.getPassword(URI.create("redis://hostname:6379"))); + } + + @Test + public void getPasswordForURIWithUsernameAndNoPassword() { + assertNull(RedisURIComponents.getPassword(URI.create("redis://username@hostname:6379"))); + } + + @Test + public void getPasswordForURIWithUsernameAndPassword() { + assertEquals("secret", RedisURIComponents.getPassword(URI.create("redis://username:secret@hostname:6379"))); + } + + @Test + public void getPasswordForURIWithPasswordAndNoUsername() { + assertEquals("secret", RedisURIComponents.getPassword(URI.create("redis://:secret@hostname:6379"))); + } + + @Test + public void getDBIndexForURIWithoutPath() { + assertEquals(0, RedisURIComponents.getDBIndex(URI.create("redis://hostname:6379"))); + } + + @Test + public void getDBIndexForURIWithRootPath() { + assertEquals(0, RedisURIComponents.getDBIndex(URI.create("redis://hostname:6379/"))); + } + + @Test + public void getDBIndexForURIWithNumberInPath() { + assertEquals(2, RedisURIComponents.getDBIndex(URI.create("redis://hostname:6379/2"))); + } +} diff --git a/lib/sdk/server/build.gradle b/lib/sdk/server/build.gradle index 94a3fb6..870e58f 100644 --- a/lib/sdk/server/build.gradle +++ b/lib/sdk/server/build.gradle @@ -22,6 +22,7 @@ plugins { id "com.github.johnrengelman.shadow" version "7.1.2" id "maven-publish" id "io.github.gradle-nexus.publish-plugin" version "1.3.0" apply false + id "org.owasp.dependencycheck" version "12.1.0" id "idea" } @@ -197,7 +198,7 @@ compileJava.dependsOn 'generateJava' jar { // thin classifier means that the non-shaded non-fat jar is still available // but is opt-in since users will have to specify it. - classifier = 'thin' + // classifier = 'thin' from configurations.commonClasses.collect { zipTree(it) } @@ -214,7 +215,7 @@ jar { // application is expected to provide SLF4J in the classpath if desired. shadowJar { // No classifier means that the shaded jar becomes the default artifact - classifier = '' + // classifier = '' configurations = [ project.configurations.internal ] @@ -242,18 +243,18 @@ shadowJar { } task testJar(type: Jar, dependsOn: testClasses) { - classifier = 'test' + // classifier = 'test' from sourceSets.test.output } // custom tasks for creating source/javadoc jars task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' + // classifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' + // classifier = 'javadoc' from javadoc.destinationDir } diff --git a/lib/sdk/server/gradle/wrapper/gradle-wrapper.properties b/lib/sdk/server/gradle/wrapper/gradle-wrapper.properties index 070cb70..2733ed5 100644 --- a/lib/sdk/server/gradle/wrapper/gradle-wrapper.properties +++ b/lib/sdk/server/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java index 96c4514..e59b822 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java @@ -4,7 +4,5 @@ abstract class Version { private Version() {} // This constant is updated automatically by our Gradle script during a release, if the project version has changed - // x-release-please-start-version static final String SDK_VERSION = "7.7.0"; - // x-release-please-end } diff --git a/release-please-config.json b/release-please-config.json index 0b476be..2b73ef2 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -11,6 +11,14 @@ "gradle.properties" ] }, + "lib/java-server-sdk-redis": { + "release-type": "simple", + "bump-minor-pre-major": true, + "include-v-in-tag": false, + "extra-files": [ + "gradle.properties" + ] + }, "lib/shared/internal": { "release-type": "simple", "bump-minor-pre-major": true, From 3d3c98a977f4ed780b200de570149ad072a7d8b0 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 13 Mar 2025 16:04:06 -0500 Subject: [PATCH 2/6] uncommenting linux workflow --- .github/workflows/java-server-sdk-redis.yml | 42 ++++++++++----------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/java-server-sdk-redis.yml b/.github/workflows/java-server-sdk-redis.yml index db598e7..046ca2d 100644 --- a/.github/workflows/java-server-sdk-redis.yml +++ b/.github/workflows/java-server-sdk-redis.yml @@ -11,30 +11,30 @@ on: - '**.md' jobs: - # build-test-java-server-sdk-redis: - # strategy: - # matrix: - # jedis-version: [2.9.0, 3.0.0] - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v3 + build-test-java-server-sdk-redis: + strategy: + matrix: + jedis-version: [2.9.0, 3.0.0] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 - # - run: | - # sudo apt-get update -y - # sudo apt-get install redis-server -y - # sudo service redis-server start + - run: | + sudo apt-get update -y + sudo apt-get install redis-server -y + sudo service redis-server start - # - name: Edit build.gradle to change Jedis version - # shell: bash - # run: | - # cd lib/java-server-sdk-redis - # sed -i.bak 's#"jedis":.*"[0-9.]*"#"jedis":"${{ matrix.jedis-version }}"#' build.gradle + - name: Edit build.gradle to change Jedis version + shell: bash + run: | + cd lib/java-server-sdk-redis + sed -i.bak 's#"jedis":.*"[0-9.]*"#"jedis":"${{ matrix.jedis-version }}"#' build.gradle - # - name: Shared CI Steps - # uses: ./.github/actions/ci - # with: - # workspace_path: 'lib/java-server-sdk-redis' - # java_version: 8 + - name: Shared CI Steps + uses: ./.github/actions/ci + with: + workspace_path: 'lib/java-server-sdk-redis' + java_version: 8 build-test-java-server-sdk-windows: strategy: From 1d4075ecd521f0f19bbf3ab9d6581d16327f03e7 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 13 Mar 2025 16:28:52 -0500 Subject: [PATCH 3/6] reverting sdk/server changes --- lib/sdk/server/build.gradle | 11 +++++------ .../server/gradle/wrapper/gradle-wrapper.properties | 2 +- .../java/com/launchdarkly/sdk/server/Version.java | 2 ++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/sdk/server/build.gradle b/lib/sdk/server/build.gradle index 870e58f..94a3fb6 100644 --- a/lib/sdk/server/build.gradle +++ b/lib/sdk/server/build.gradle @@ -22,7 +22,6 @@ plugins { id "com.github.johnrengelman.shadow" version "7.1.2" id "maven-publish" id "io.github.gradle-nexus.publish-plugin" version "1.3.0" apply false - id "org.owasp.dependencycheck" version "12.1.0" id "idea" } @@ -198,7 +197,7 @@ compileJava.dependsOn 'generateJava' jar { // thin classifier means that the non-shaded non-fat jar is still available // but is opt-in since users will have to specify it. - // classifier = 'thin' + classifier = 'thin' from configurations.commonClasses.collect { zipTree(it) } @@ -215,7 +214,7 @@ jar { // application is expected to provide SLF4J in the classpath if desired. shadowJar { // No classifier means that the shaded jar becomes the default artifact - // classifier = '' + classifier = '' configurations = [ project.configurations.internal ] @@ -243,18 +242,18 @@ shadowJar { } task testJar(type: Jar, dependsOn: testClasses) { - // classifier = 'test' + classifier = 'test' from sourceSets.test.output } // custom tasks for creating source/javadoc jars task sourcesJar(type: Jar, dependsOn: classes) { - // classifier = 'sources' + classifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn: javadoc) { - // classifier = 'javadoc' + classifier = 'javadoc' from javadoc.destinationDir } diff --git a/lib/sdk/server/gradle/wrapper/gradle-wrapper.properties b/lib/sdk/server/gradle/wrapper/gradle-wrapper.properties index 2733ed5..070cb70 100644 --- a/lib/sdk/server/gradle/wrapper/gradle-wrapper.properties +++ b/lib/sdk/server/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java index e59b822..96c4514 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java @@ -4,5 +4,7 @@ abstract class Version { private Version() {} // This constant is updated automatically by our Gradle script during a release, if the project version has changed + // x-release-please-start-version static final String SDK_VERSION = "7.7.0"; + // x-release-please-end } From 4400b965598490be329d565f906806849f255ca5 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 14 Mar 2025 09:53:14 -0500 Subject: [PATCH 4/6] more tweaks --- .../package-redis-store--bug_report.md | 36 ++++++++++++++++++ .../package-redis-store--feature_request.md | 20 ++++++++++ .github/actions/ci/action.yml | 8 ++++ ...is.yml => java-server-sdk-redis-store.yml} | 17 +++------ .github/workflows/manual-publish-docs.yml | 3 +- .github/workflows/manual-publish.yml | 3 +- .github/workflows/release-please.yml | 35 ++++++++++++++++- .release-please-manifest.json | 2 +- README.md | 16 ++++---- .../CHANGELOG.md | 2 +- .../README.md | 3 -- .../build.gradle | 0 .../checkstyle.xml | 0 .../gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../gradlew | 0 .../gradlew.bat | 0 .../settings.gradle | 0 .../sdk/server/integrations/Redis.java | 0 .../RedisBigSegmentStoreImpl.java | 0 .../integrations/RedisDataStoreImpl.java | 0 .../integrations/RedisStoreBuilder.java | 0 .../integrations/RedisStoreImplBase.java | 0 .../integrations/RedisURIComponents.java | 0 .../RedisBigSegmentStoreImplTest.java | 0 .../RedisDataStoreBuilderTest.java | 0 .../integrations/RedisDataStoreImplTest.java | 0 .../integrations/RedisURIComponentsTest.java | 0 release-please-config.json | 2 +- 30 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/package-redis-store--bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/package-redis-store--feature_request.md rename .github/workflows/{java-server-sdk-redis.yml => java-server-sdk-redis-store.yml} (79%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/CHANGELOG.md (97%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/README.md (93%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/build.gradle (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/checkstyle.xml (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/gradle.properties (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/gradle/wrapper/gradle-wrapper.jar (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/gradle/wrapper/gradle-wrapper.properties (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/gradlew (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/gradlew.bat (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/settings.gradle (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/src/main/java/com/launchdarkly/sdk/server/integrations/Redis.java (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/src/main/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImpl.java (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/src/main/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImpl.java (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/src/test/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImplTest.java (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreBuilderTest.java (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImplTest.java (100%) rename lib/{java-server-sdk-redis => java-server-sdk-redis-store}/src/test/java/com/launchdarkly/sdk/server/integrations/RedisURIComponentsTest.java (100%) diff --git a/.github/ISSUE_TEMPLATE/package-redis-store--bug_report.md b/.github/ISSUE_TEMPLATE/package-redis-store--bug_report.md new file mode 100644 index 0000000..2546fd2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/package-redis-store--bug_report.md @@ -0,0 +1,36 @@ +--- +name: 'Bug report for the java-server-sdk-otel package' +about: Create a report to help us improve +title: '' +labels: 'package: java-server-sdk-otel, bug' +assignees: '' +--- + +**Is this a support request?** +This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the code in this library. If you're not sure whether the problem you are having is specifically related to this library, 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/) and clicking "submit a request", or by emailing support@launchdarkly.com. + +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. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To reproduce** +Steps to reproduce the behavior. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +If applicable, add any log output related to your problem. + +**SDK version** +The version of this SDK that you are using. + +**Language version, developer tools** +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. + +**OS/platform** +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. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/package-redis-store--feature_request.md b/.github/ISSUE_TEMPLATE/package-redis-store--feature_request.md new file mode 100644 index 0000000..5188ff2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/package-redis-store--feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request for the java-server-sdk-redis-store package +about: Suggest an idea for this project +title: '' +labels: 'package: java-server-sdk-redis-store, enhancement' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I would love to see the SDK [...does something new...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context about the feature request here. \ No newline at end of file diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 5fc5754..910fe8e 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -27,6 +27,14 @@ runs: distribution: ${{ inputs.java_distribution }} java-version: ${{ inputs.java_version }} + - name: Setup Redis Service + shell: bash + if: ${{ inputs.workspace_path == 'lib/java-server-sdk-redis-store' }} + run: | + sudo apt-get update -y + sudo apt-get install redis-server -y + sudo service redis-server start + - name: Restore dependencies shell: bash id: restore diff --git a/.github/workflows/java-server-sdk-redis.yml b/.github/workflows/java-server-sdk-redis-store.yml similarity index 79% rename from .github/workflows/java-server-sdk-redis.yml rename to .github/workflows/java-server-sdk-redis-store.yml index 046ca2d..05cf5bb 100644 --- a/.github/workflows/java-server-sdk-redis.yml +++ b/.github/workflows/java-server-sdk-redis-store.yml @@ -1,4 +1,4 @@ -name: java-server-sdk-redis +name: java-server-sdk-redis-store on: push: @@ -11,7 +11,7 @@ on: - '**.md' jobs: - build-test-java-server-sdk-redis: + build-test-java-server-sdk-redis-store: strategy: matrix: jedis-version: [2.9.0, 3.0.0] @@ -19,21 +19,16 @@ jobs: steps: - uses: actions/checkout@v3 - - run: | - sudo apt-get update -y - sudo apt-get install redis-server -y - sudo service redis-server start - - name: Edit build.gradle to change Jedis version shell: bash run: | - cd lib/java-server-sdk-redis + cd lib/java-server-sdk-redis-store sed -i.bak 's#"jedis":.*"[0-9.]*"#"jedis":"${{ matrix.jedis-version }}"#' build.gradle - name: Shared CI Steps uses: ./.github/actions/ci with: - workspace_path: 'lib/java-server-sdk-redis' + workspace_path: 'lib/java-server-sdk-redis-store' java_version: 8 build-test-java-server-sdk-windows: @@ -58,11 +53,11 @@ jobs: - name: Edit build.gradle to change Jedis version shell: bash run: | - cd lib/java-server-sdk-redis + cd lib/java-server-sdk-redis-store sed -i.bak 's#"jedis":.*"[0-9.]*"#"jedis":"${{ matrix.jedis-version }}"#' build.gradle - name: Shared CI Steps uses: ./.github/actions/ci with: - workspace_path: 'lib/java-server-sdk-redis' + workspace_path: 'lib/java-server-sdk-redis-store' java_version: 8 diff --git a/.github/workflows/manual-publish-docs.yml b/.github/workflows/manual-publish-docs.yml index 9cc5052..c011c70 100644 --- a/.github/workflows/manual-publish-docs.yml +++ b/.github/workflows/manual-publish-docs.yml @@ -6,10 +6,11 @@ on: required: true type: choice options: - - lib/java-server-sdk-otel - lib/shared/common - lib/shared/internal - lib/sdk/server + - lib/java-server-sdk-otel + - lib/java-server-sdk-redis-store dry_run: description: 'Is this a dry run. If so no docs will be published.' type: boolean diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 2e88854..fed4b9e 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -7,10 +7,11 @@ on: required: true type: choice options: - - lib/java-server-sdk-otel - lib/shared/common - lib/shared/internal - lib/sdk/server + - lib/java-server-sdk-otel + - lib/java-server-sdk-redis-store prerelease: description: 'Is this a prerelease.' type: boolean diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 6b592d1..4aae3b2 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -10,10 +10,11 @@ jobs: runs-on: ubuntu-latest outputs: - package-server-sdk-otel-released: ${{ steps.release.outputs['lib/java-server-sdk-otel--release_created'] }} package-sdk-common-released: ${{ steps.release.outputs['lib/java-sdk-common--release_created'] }} package-sdk-internal-released: ${{ steps.release.outputs['lib/java-sdk-internal--release_created'] }} package-server-sdk-released: ${{ steps.release.outputs['lib/java-server-sdk--release_created'] }} + package-server-sdk-otel-released: ${{ steps.release.outputs['lib/java-server-sdk-otel--release_created'] }} + package-server-sdk-redis-store-released: ${{ steps.release.outputs['lib/java-server-sdk-redis-store--release_created'] }} steps: - uses: google-github-actions/release-please-action@v4 @@ -86,6 +87,38 @@ jobs: aws_role: ${{ vars.AWS_ROLE_ARN }} token: ${{ secrets.GITHUB_TOKEN }} + release-server-sdk-redis-store: + runs-on: ubuntu-latest + needs: release-please + permissions: + id-token: write + contents: write + pull-requests: write + if: ${{ needs.release-please.outputs.package-server-sdk-redis-store-released == 'true'}} + steps: + - uses: actions/checkout@v4 + + - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.1.0 + name: Get secrets + with: + aws_assume_role: ${{ vars.AWS_ROLE_ARN }} + ssm_parameter_pairs: '/production/common/releasing/sonatype/username = SONATYPE_USER_NAME, + /production/common/releasing/sonatype/password = SONATYPE_PASSWORD' + s3_path_pairs: 'launchdarkly-releaser/java/code-signing-keyring.gpg = code-signing-keyring.gpg' + + - uses: ./.github/actions/full-release + with: + workspace_path: lib/java-server-sdk-redis-store + dry_run: false + prerelease: false + code_signing_keyring: 'code-signing-keyring.gpg' + signing_key_id: ${{ env.SIGNING_KEY_ID }} + signing_key_passphrase: ${{ env.SIGNING_KEY_PASSPHRASE }} + sonatype_username: ${{ env.SONATYPE_USER_NAME }} + sonatype_password: ${{ env.SONATYPE_PASSWORD }} + aws_role: ${{ vars.AWS_ROLE_ARN }} + token: ${{ secrets.GITHUB_TOKEN }} + release-sdk-internal: runs-on: ubuntu-latest needs: release-please diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 995d19c..3ea9764 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { "lib/java-server-sdk-otel": "0.1.0", - "lib/java-server-sdk-redis": "3.0.0", + "lib/java-server-sdk-redis-store": "3.0.0", "lib/shared/common": "2.1.1", "lib/shared/internal": "1.3.0", "lib/sdk/server": "7.7.0" diff --git a/README.md b/README.md index 72d39f5..2a05665 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This includes shared libraries, used by SDKs and other tools, as well as SDKs. | Other Packages | API Docs | maven | issues | tests | | ---------------------------------------------------------------------------- |--------------------------------------------------------------| ---------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------- | | [@launchdarkly/java-server-sdk-otel](lib/java-server-sdk-otel/README.md) | [![Documentation][server-otel-docs-badge]][server-otel-docs-link] | [![maven][server-otel-maven-badge]][server-otel-maven-link] | [Issues][server-otel-issues] | [![Actions Status][server-otel-ci-badge]][server-otel-ci-link] | -| [@launchdarkly/java-server-sdk-redis](lib/java-server-sdk-redis/README.md) | [![Documentation][server-redis-docs-badge]][server-redis-docs-link] | [![maven][server-redis-maven-badge]][server-redis-maven-link] | [Issues][server-redis-issues] | [![Actions Status][server-redis-ci-badge]][server-redis-ci-link] | +| [@launchdarkly/java-server-sdk-redis-store](lib/java-server-sdk-redis-store/README.md) | [![Documentation][server-redis-docs-badge]][server-redis-docs-link] | [![maven][server-redis-maven-badge]][server-redis-maven-link] | [Issues][server-redis-issues] | [![Actions Status][server-redis-ci-badge]][server-redis-ci-link] | ## Organization @@ -72,14 +72,14 @@ We encourage pull requests and other contributions from the community. Check out [server-otel-docs-badge]: https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8 [server-otel-docs-link]: https://launchdarkly.github.io/java-core/lib/java-server-sdk-otel/ -[//]: # 'java-server-sdk-redis' -[server-redis-issues]: https://github.com/launchdarkly/java-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+java-server-sdk-redis%22+ -[server-redis-maven-badge]: https://img.shields.io/maven-central/v/com.launchdarkly/launchdarkly-java-server-sdk-redis -[server-redis-maven-link]: https://central.sonatype.com/artifact/com.launchdarkly/launchdarkly-java-server-sdk-redis -[server-redis-ci-badge]: https://github.com/launchdarkly/java-core/actions/workflows/java-server-sdk-redis.yml/badge.svg -[server-redis-ci-link]: https://github.com/launchdarkly/java-core/actions/workflows/java-server-sdk-redis.yml +[//]: # 'java-server-sdk-redis-store' +[server-redis-issues]: https://github.com/launchdarkly/java-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+java-server-sdk-redis-store%22+ +[server-redis-maven-badge]: https://img.shields.io/maven-central/v/com.launchdarkly/launchdarkly-java-server-sdk-redis-store +[server-redis-maven-link]: https://central.sonatype.com/artifact/com.launchdarkly/launchdarkly-java-server-sdk-redis-store +[server-redis-ci-badge]: https://github.com/launchdarkly/java-core/actions/workflows/java-server-sdk-redis-store.yml/badge.svg +[server-redis-ci-link]: https://github.com/launchdarkly/java-core/actions/workflows/java-server-sdk-redis-store.yml [server-redis-docs-badge]: https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8 -[server-redis-docs-link]: https://launchdarkly.github.io/java-core/lib/java-server-sdk-redis/ +[server-redis-docs-link]: https://launchdarkly.github.io/java-core/lib/java-server-sdk-redis-store/ [//]: # 'java-sdk-internal' [sdk-internal-issues]: https://github.com/launchdarkly/java-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+java-sdk-internal%22+ diff --git a/lib/java-server-sdk-redis/CHANGELOG.md b/lib/java-server-sdk-redis-store/CHANGELOG.md similarity index 97% rename from lib/java-server-sdk-redis/CHANGELOG.md rename to lib/java-server-sdk-redis-store/CHANGELOG.md index 6bbc3f4..23dcd29 100644 --- a/lib/java-server-sdk-redis/CHANGELOG.md +++ b/lib/java-server-sdk-redis-store/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to the LaunchDarkly Java SDK Redis integration will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). ## [3.0.0] - 2022-12-07 -This release corresponds to the 6.0.0 release of the LaunchDarkly Java SDK. Any application code that is being updated to use the 6.0.0 SDK, and was using a 2.x version of `launchdarkly-java-server-sdk-redis`, should now use a 3.x version instead. +This release corresponds to the 6.0.0 release of the LaunchDarkly Java SDK. Any application code that is being updated to use the 6.0.0 SDK, and was using a 2.x version of `launchdarkly-java-server-sdk-redis-store`, should now use a 3.x version instead. There are no functional differences in the behavior of the Redis integration; the differences are only related to changes in the usage of interface types for configuration in the SDK. diff --git a/lib/java-server-sdk-redis/README.md b/lib/java-server-sdk-redis-store/README.md similarity index 93% rename from lib/java-server-sdk-redis/README.md rename to lib/java-server-sdk-redis-store/README.md index 94a1dc8..d2d2820 100644 --- a/lib/java-server-sdk-redis/README.md +++ b/lib/java-server-sdk-redis-store/README.md @@ -1,8 +1,5 @@ # LaunchDarkly SDK for Java - Redis integration -[![Circle CI](https://circleci.com/gh/launchdarkly/java-server-sdk-redis.svg?style=shield)](https://circleci.com/gh/launchdarkly/java-server-sdk-redis) -[![Javadocs](http://javadoc.io/badge/com.launchdarkly/launchdarkly-java-server-sdk-redis-store.svg)](http://javadoc.io/doc/com.launchdarkly/launchdarkly-java-server-sdk-redis-store) - This library provides a Redis-backed persistence mechanism (feature store) for the [LaunchDarkly Java SDK](https://github.com/launchdarkly/java-server-sdk), replacing the default in-memory feature store. The Redis API implementation it uses is [Jedis](https://github.com/xetorthio/jedis). This version of the library requires at least version 6.0.0 of the LaunchDarkly Java SDK; for versions of the library to use with earlier SDK versions, see the changelog. The minimum Java version is 8. diff --git a/lib/java-server-sdk-redis/build.gradle b/lib/java-server-sdk-redis-store/build.gradle similarity index 100% rename from lib/java-server-sdk-redis/build.gradle rename to lib/java-server-sdk-redis-store/build.gradle diff --git a/lib/java-server-sdk-redis/checkstyle.xml b/lib/java-server-sdk-redis-store/checkstyle.xml similarity index 100% rename from lib/java-server-sdk-redis/checkstyle.xml rename to lib/java-server-sdk-redis-store/checkstyle.xml diff --git a/lib/java-server-sdk-redis/gradle.properties b/lib/java-server-sdk-redis-store/gradle.properties similarity index 100% rename from lib/java-server-sdk-redis/gradle.properties rename to lib/java-server-sdk-redis-store/gradle.properties diff --git a/lib/java-server-sdk-redis/gradle/wrapper/gradle-wrapper.jar b/lib/java-server-sdk-redis-store/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from lib/java-server-sdk-redis/gradle/wrapper/gradle-wrapper.jar rename to lib/java-server-sdk-redis-store/gradle/wrapper/gradle-wrapper.jar diff --git a/lib/java-server-sdk-redis/gradle/wrapper/gradle-wrapper.properties b/lib/java-server-sdk-redis-store/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from lib/java-server-sdk-redis/gradle/wrapper/gradle-wrapper.properties rename to lib/java-server-sdk-redis-store/gradle/wrapper/gradle-wrapper.properties diff --git a/lib/java-server-sdk-redis/gradlew b/lib/java-server-sdk-redis-store/gradlew similarity index 100% rename from lib/java-server-sdk-redis/gradlew rename to lib/java-server-sdk-redis-store/gradlew diff --git a/lib/java-server-sdk-redis/gradlew.bat b/lib/java-server-sdk-redis-store/gradlew.bat similarity index 100% rename from lib/java-server-sdk-redis/gradlew.bat rename to lib/java-server-sdk-redis-store/gradlew.bat diff --git a/lib/java-server-sdk-redis/settings.gradle b/lib/java-server-sdk-redis-store/settings.gradle similarity index 100% rename from lib/java-server-sdk-redis/settings.gradle rename to lib/java-server-sdk-redis-store/settings.gradle diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/Redis.java b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/Redis.java similarity index 100% rename from lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/Redis.java rename to lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/Redis.java diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImpl.java b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImpl.java similarity index 100% rename from lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImpl.java rename to lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImpl.java diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImpl.java b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImpl.java similarity index 100% rename from lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImpl.java rename to lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImpl.java diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java similarity index 100% rename from lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java rename to lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java similarity index 100% rename from lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java rename to lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java diff --git a/lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java similarity index 100% rename from lib/java-server-sdk-redis/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java rename to lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java diff --git a/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImplTest.java b/lib/java-server-sdk-redis-store/src/test/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImplTest.java similarity index 100% rename from lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImplTest.java rename to lib/java-server-sdk-redis-store/src/test/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImplTest.java diff --git a/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreBuilderTest.java b/lib/java-server-sdk-redis-store/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreBuilderTest.java similarity index 100% rename from lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreBuilderTest.java rename to lib/java-server-sdk-redis-store/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreBuilderTest.java diff --git a/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImplTest.java b/lib/java-server-sdk-redis-store/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImplTest.java similarity index 100% rename from lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImplTest.java rename to lib/java-server-sdk-redis-store/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImplTest.java diff --git a/lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisURIComponentsTest.java b/lib/java-server-sdk-redis-store/src/test/java/com/launchdarkly/sdk/server/integrations/RedisURIComponentsTest.java similarity index 100% rename from lib/java-server-sdk-redis/src/test/java/com/launchdarkly/sdk/server/integrations/RedisURIComponentsTest.java rename to lib/java-server-sdk-redis-store/src/test/java/com/launchdarkly/sdk/server/integrations/RedisURIComponentsTest.java diff --git a/release-please-config.json b/release-please-config.json index 2b73ef2..aafc294 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -11,7 +11,7 @@ "gradle.properties" ] }, - "lib/java-server-sdk-redis": { + "lib/java-server-sdk-redis-store": { "release-type": "simple", "bump-minor-pre-major": true, "include-v-in-tag": false, From c30506a7c1f48a16c0f4184ee6c28601327f982f Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 14 Mar 2025 09:54:51 -0500 Subject: [PATCH 5/6] more tweaks --- .github/ISSUE_TEMPLATE/package-redis-store--bug_report.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/package-redis-store--bug_report.md b/.github/ISSUE_TEMPLATE/package-redis-store--bug_report.md index 2546fd2..1cfa416 100644 --- a/.github/ISSUE_TEMPLATE/package-redis-store--bug_report.md +++ b/.github/ISSUE_TEMPLATE/package-redis-store--bug_report.md @@ -1,8 +1,8 @@ --- -name: 'Bug report for the java-server-sdk-otel package' +name: 'Bug report for the java-server-sdk-redis-store package' about: Create a report to help us improve title: '' -labels: 'package: java-server-sdk-otel, bug' +labels: 'package: java-server-sdk-redis-store, bug' assignees: '' --- From 186f9849626add80ce48899d822f6deba9057aae Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 14 Mar 2025 10:00:15 -0500 Subject: [PATCH 6/6] more tweaks --- .../workflows/java-server-sdk-redis-store.yml | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/java-server-sdk-redis-store.yml b/.github/workflows/java-server-sdk-redis-store.yml index 05cf5bb..6e4a3d7 100644 --- a/.github/workflows/java-server-sdk-redis-store.yml +++ b/.github/workflows/java-server-sdk-redis-store.yml @@ -56,8 +56,32 @@ jobs: cd lib/java-server-sdk-redis-store sed -i.bak 's#"jedis":.*"[0-9.]*"#"jedis":"${{ matrix.jedis-version }}"#' build.gradle - - name: Shared CI Steps - uses: ./.github/actions/ci + - name: Setup Java + uses: actions/setup-java@v4 with: - workspace_path: 'lib/java-server-sdk-redis-store' - java_version: 8 + distribution: 'temurin' + java-version: 8 + + - name: Restore dependencies + shell: bash + id: restore + run: lib/java-server-sdk-redis-store/gradlew dependencies -p lib/java-server-sdk-redis-store + + - name: Build + shell: bash + id: build + run: lib/java-server-sdk-redis-store/gradlew build -p lib/java-server-sdk-redis-store + + - name: Build Jar + shell: bash + id: buildjar + run: lib/java-server-sdk-redis-store/gradlew jar -p lib/java-server-sdk-redis-store + + - name: Run Tests + if: steps.build.outcome == 'success' && inputs.run_tests == 'true' + shell: bash + run: lib/java-server-sdk-redis-store/gradlew test -p lib/java-server-sdk-redis-store + + - name: Build Documentation + shell: bash + run: lib/java-server-sdk-redis-store/gradlew javadoc -p lib/java-server-sdk-redis-store