Skip to content

Commit eff9e4e

Browse files
pditommasoclaude
andcommitted
[release] lib-data-range-redis@1.0.0
Initial release of lib-data-range-redis module with pure Java implementation: - RangeStore interface for time-based range storage similar to Redis ZRANGE - LocalRangeProvider for in-memory storage - RedisRangeProvider using Redis sorted sets - Add module to CI workflows and dependency scanning Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 17b8651 commit eff9e4e

File tree

16 files changed

+675
-0
lines changed

16 files changed

+675
-0
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
bash publish.sh lib-crypto
6969
bash publish.sh lib-docker-cli
7070
bash publish.sh lib-data-queue-redis
71+
bash publish.sh lib-data-range-redis
7172
bash publish.sh lib-data-store-future-redis
7273
bash publish.sh lib-data-store-state-redis
7374
bash publish.sh lib-data-stream-redis

.github/workflows/generate-submit-dependencies.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
"lib-crypto",
2020
"lib-docker-cli",
2121
"lib-data-queue-redis",
22+
"lib-data-range-redis",
2223
"lib-data-store-future-redis",
2324
"lib-data-store-state-redis",
2425
"lib-data-stream-redis",

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ A collection of reusable Java & Groovy libraries for Seqera platform components.
2323
### Messaging & Data Libraries
2424
- **[lib-cache-tiered-redis](lib-cache-tiered-redis/)** - Two-tier caching with Caffeine (L1) and Redis (L2) for distributed caching
2525
- **[lib-data-queue-redis](lib-data-queue-redis/)** - Message queue abstraction with Redis and local implementations
26+
- **[lib-data-range-redis](lib-data-range-redis/)** - Range set storage similar to Redis ZRANGE with local and Redis implementations
2627
- **[lib-data-store-future-redis](lib-data-store-future-redis/)** - Distributed CompletableFuture store for cross-service async operations
2728
- **[lib-data-store-state-redis](lib-data-store-state-redis/)** - Distributed state store with atomic operations and counters
2829
- **[lib-data-stream-redis](lib-data-stream-redis/)** - Message streaming with Redis Streams and local implementations

lib-data-range-redis/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# lib-data-range-redis
2+
3+
Time-based range store abstraction with Redis and local implementations. Useful for implementing cron-like services that need to schedule and retrieve entries based on expiration time.
4+
5+
## Installation
6+
7+
Add this dependency to your `build.gradle`:
8+
9+
```gradle
10+
dependencies {
11+
implementation 'io.seqera:lib-data-range-redis:1.0.0'
12+
}
13+
```
14+
15+
## Usage
16+
17+
The library provides a `RangeStore` interface for storing entries with a timestamp score, allowing efficient retrieval of entries within a time range. This is particularly useful for implementing scheduled cleanup services or cron-like background tasks.
18+
19+
### Creating a Store
20+
21+
Extend `AbstractRangeStore` to create a concrete store for your use case:
22+
23+
```groovy
24+
@Singleton
25+
@CompileStatic
26+
class ScheduledTaskStore extends AbstractRangeStore {
27+
28+
ScheduledTaskStore(RangeProvider provider) {
29+
super(provider)
30+
}
31+
32+
@Override
33+
protected String getKey() {
34+
return 'scheduled-tasks/v1:'
35+
}
36+
}
37+
```
38+
39+
### Implementing a Cron Service
40+
41+
Use the store with a scheduler to implement periodic background tasks:
42+
43+
```groovy
44+
@Slf4j
45+
@Context
46+
@CompileStatic
47+
class ScheduledTaskService implements Runnable {
48+
49+
@Inject
50+
private TaskScheduler scheduler
51+
52+
@Inject
53+
private ScheduledTaskStore store
54+
55+
@PostConstruct
56+
private init() {
57+
// Schedule periodic execution
58+
scheduler.scheduleWithFixedDelay(
59+
Duration.ofMinutes(1), // initial delay
60+
Duration.ofMinutes(5), // interval
61+
this)
62+
}
63+
64+
@Override
65+
void run() {
66+
final now = Instant.now()
67+
// Retrieve entries with score <= current time (expired entries)
68+
final keys = store.getRange(0, now.epochSecond, 100)
69+
70+
for (String entry : keys) {
71+
processEntry(entry)
72+
}
73+
}
74+
75+
// Schedule an entry for future processing
76+
void scheduleTask(String taskId, Duration delay) {
77+
final expirationSecs = Instant.now().plus(delay).epochSecond
78+
store.add(taskId, expirationSecs)
79+
}
80+
81+
private void processEntry(String entry) {
82+
// Handle the expired entry
83+
log.debug "Processing entry: $entry"
84+
}
85+
}
86+
```
87+
88+
### Key Methods
89+
90+
- `add(String member, double score)` - Add an entry with a timestamp score
91+
- `getRange(double min, double max, int count)` - Retrieve entries within a score range
92+
93+
The score typically represents epoch seconds, making it easy to schedule entries for future processing.
94+
95+
## Configuration
96+
97+
For Redis implementation, configure the Redis URI in your `application.yml`:
98+
99+
```yaml
100+
redis:
101+
uri: redis://localhost:6379
102+
```
103+
104+
The library automatically selects `RedisRangeProvider` when `redis.uri` is configured, otherwise falls back to `LocalRangeProvider` for development/testing.
105+
106+
## Testing
107+
108+
```bash
109+
./gradlew :lib-data-range-redis:test
110+
```

lib-data-range-redis/VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.0.0

lib-data-range-redis/build.gradle

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
plugins {
19+
id 'io.seqera.java-library-conventions'
20+
id 'io.seqera.groovy-library-conventions'
21+
// Micronaut minimal lib
22+
// https://micronaut-projects.github.io/micronaut-gradle-plugin/latest/
23+
id "io.micronaut.minimal.library" version '4.5.3'
24+
}
25+
26+
group = 'io.seqera'
27+
version = "${project.file('VERSION').text.trim()}"
28+
29+
dependencies {
30+
implementation 'redis.clients:jedis:5.1.4'
31+
32+
compileOnly "io.micronaut:micronaut-inject:${micronautCoreVersion}"
33+
implementation "ch.qos.logback:logback-classic:1.5.19"
34+
implementation "io.micronaut:micronaut-runtime:${micronautCoreVersion}"
35+
36+
testImplementation testFixtures(project(':lib-fixtures-redis'))
37+
testImplementation "org.apache.groovy:groovy:4.0.24"
38+
testImplementation "io.micronaut:micronaut-inject-groovy:${micronautCoreVersion}"
39+
testImplementation "io.micronaut.test:micronaut-test-spock:${micronautTestVersion}"
40+
testImplementation 'org.testcontainers:testcontainers:1.20.6'
41+
testRuntimeOnly 'org.yaml:snakeyaml:2.2'
42+
}
43+
44+
test {
45+
useJUnitPlatform()
46+
}
47+
48+
49+
micronaut {
50+
version '4.8.3'
51+
runtime("netty")
52+
processing {
53+
incremental(true)
54+
}
55+
importMicronautPlatform = false
56+
}

lib-data-range-redis/changelog.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# lib-data-range-redis changelog
2+
3+
1.0.0 - 14 Jan 2025
4+
- Initial release of lib-data-range-redis extracted from Wave project
5+
- Implement RangeStore interface for time-based range storage
6+
- Add AbstractRangeStore base class for concrete store implementations
7+
- Add RangeProvider interface for storage backend abstraction
8+
- Add LocalRangeProvider for in-memory range storage
9+
- Add RedisRangeProvider implementation using Redis sorted sets
10+
- Support scheduling entries with timestamp scores for cron-like services
11+
- Use @Requires(property = 'redis.uri') for conditional bean loading
12+
- Include comprehensive test suite with Redis testcontainers
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package io.seqera.data.range;
19+
20+
import java.util.List;
21+
22+
import io.seqera.data.range.impl.RangeProvider;
23+
24+
/**
25+
* Abstract implementation for range set similar to Redis {@code zrange}
26+
*
27+
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
28+
*/
29+
public abstract class AbstractRangeStore implements RangeStore {
30+
31+
private final RangeProvider delegate;
32+
33+
protected abstract String getKey();
34+
35+
protected AbstractRangeStore(RangeProvider provider) {
36+
this.delegate = provider;
37+
}
38+
39+
@Override
40+
public void add(String name, double score) {
41+
delegate.add(getKey(), name, score);
42+
}
43+
44+
@Override
45+
public List<String> getRange(double min, double max, int count) {
46+
return getRange(min, max, count, true);
47+
}
48+
49+
public List<String> getRange(double min, double max, int count, boolean remove) {
50+
List<String> result = delegate.getRange(getKey(), min, max, count, remove);
51+
return result != null ? result : List.of();
52+
}
53+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package io.seqera.data.range;
19+
20+
import java.util.List;
21+
22+
/**
23+
* Define the contract for a storage range set similar to Redis {@code zrange}
24+
*
25+
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
26+
*/
27+
public interface RangeStore {
28+
29+
void add(String member, double score);
30+
31+
List<String> getRange(double min, double max, int count);
32+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package io.seqera.data.range.impl;
19+
20+
import java.util.ArrayList;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import io.micronaut.context.annotation.Requires;
26+
import jakarta.inject.Singleton;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
30+
/**
31+
* Local based implementation for a range set
32+
*
33+
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
34+
*/
35+
@Requires(missingProperty = "redis.uri")
36+
@Singleton
37+
public class LocalRangeProvider implements RangeProvider {
38+
39+
private static final Logger log = LoggerFactory.getLogger(LocalRangeProvider.class);
40+
41+
private final Map<String, Map<String, Double>> store = new HashMap<>();
42+
43+
@Override
44+
public void add(String key, String element, double score) {
45+
Map<String, Double> map = store.computeIfAbsent(key, k -> new HashMap<>());
46+
map.put(element, score);
47+
log.trace("* add range - store: {}", store);
48+
}
49+
50+
@Override
51+
public List<String> getRange(String key, double min, double max, int count, boolean remove) {
52+
Map<String, Double> map = store.getOrDefault(key, new HashMap<>());
53+
List<String> result = new ArrayList<>();
54+
55+
// Sort entries by value and iterate
56+
List<Map.Entry<String, Double>> sortedEntries = new ArrayList<>(map.entrySet());
57+
sortedEntries.sort(Map.Entry.comparingByValue());
58+
59+
List<String> toRemove = new ArrayList<>();
60+
for (Map.Entry<String, Double> entry : sortedEntries) {
61+
if (result.size() >= count) {
62+
break;
63+
}
64+
if (entry.getValue() >= min && entry.getValue() <= max) {
65+
result.add(entry.getKey());
66+
if (remove) {
67+
toRemove.add(entry.getKey());
68+
}
69+
}
70+
}
71+
72+
// Remove after iteration to avoid ConcurrentModificationException
73+
for (String k : toRemove) {
74+
map.remove(k);
75+
}
76+
77+
log.trace("* get range result={} - store: {}", result, store);
78+
return result;
79+
}
80+
}

0 commit comments

Comments
 (0)