Skip to content

Commit 4bae458

Browse files
Support sinking arbitrary Redis commands (#16)
1 parent 921e36c commit 4bae458

File tree

17 files changed

+295
-18
lines changed

17 files changed

+295
-18
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [1.0.4] - 2020-11-29
8+
### Added
9+
- Added support for sinking arbitrary Redis commands, primarily for use with Redis modules
10+
11+
### Fixed
12+
- Fixed linter configuration
13+
714
## [1.0.3] - 2020-11-21
815
### Added
916
- Added support for Redis EXPIRE commands

checkstyle.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
<property name="allowNonPrintableEscapes" value="true"/>
6262
</module>
6363
<module name="AvoidStarImport"/>
64+
<module name="UnusedImports"/>
6465
<module name="OneTopLevelClass"/>
6566
<module name="NoLineWrap">
6667
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>

docs/connectors/SINK.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The following commands are supported at this time:
88
- [PEXPIRE](https://redis.io/commands/pexpire)
99
- [SADD](https://redis.io/commands/sadd)
1010
- [GEOADD](https://redis.io/commands/geoadd)
11+
- Arbitrary -- useful for Redis modules
1112

1213
Support for additional write-based commands will be added in the future.
1314

@@ -376,6 +377,52 @@ Keys are ignored.
376377
}
377378
```
378379

380+
#### Arbitrary
381+
##### Avro
382+
```json
383+
{
384+
"namespace": "io.github.jaredpetersen.kafkaconnectredis",
385+
"name": "RedisArbitraryCommand",
386+
"type": "record",
387+
"fields": [
388+
{
389+
"name": "command",
390+
"type": "string"
391+
},
392+
{
393+
"name": "arguments",
394+
"type": {
395+
"type": "array",
396+
"items": "string"
397+
}
398+
}
399+
]
400+
}
401+
```
402+
403+
##### Connect JSON
404+
```json
405+
{
406+
"name": "io.github.jaredpetersen.kafkaconnectredis.RedisArbitraryCommand",
407+
"type": "struct",
408+
"fields": [
409+
{
410+
"field": "command",
411+
"type": "string",
412+
"optional": false
413+
},
414+
{
415+
"field": "arguments",
416+
"type": "array",
417+
"items": {
418+
"type": "string"
419+
},
420+
"optional": false
421+
}
422+
]
423+
}
424+
```
425+
379426
## Configuration
380427
### Connector Properties
381428
| Name | Type | Default | Importance | Description |

docs/demo/README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,23 @@ minikube start --cpus 2 --memory 10g
1111
```
1212

1313
### Docker
14-
Now that we have Kubernetes set up locally, we'll need a Docker image that contains Kafka Connect Redis.
14+
Now that we have Kubernetes set up locally, we'll need some Docker images.
1515

16-
Navigate to `demo/docker/` in this repository and run the following commands **in a separate terminal** to download the plugin and build the image for minikube:
16+
Open a new terminal we can use to build images for minikube. Run the following command to connect the terminal to minikube:
1717
```bash
18-
curl -O https://oss.sonatype.org/service/local/repositories/releases/content/io/github/jaredpetersen/kafka-connect-redis/1.0.3/kafka-connect-redis-1.0.3.jar
1918
eval $(minikube docker-env)
19+
```
20+
21+
We'll use this terminal for the rest of this section.
22+
23+
Let's start by building Redis. Navigate to `demo/docker/redis` and run the following commands:
24+
```bash
25+
docker build -t jaredpetersen/redis:latest .
26+
```
27+
28+
Next, we'll need to build a docker image for Kafka Connect Redis. Navigate to `demo/docker/kafka-connect-redis` and run the following commands:
29+
```bash
30+
curl -O https://oss.sonatype.org/service/local/repositories/releases/content/io/github/jaredpetersen/kafka-connect-redis/1.0.4/kafka-connect-redis-1.0.4.jar
2031
docker build -t jaredpetersen/kafka-connect-redis:latest .
2132
```
2233

docs/demo/SINK.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ curl --request POST \
1818
"value.converter": "io.confluent.connect.avro.AvroConverter",
1919
"value.converter.schema.registry.url": "http://kafka-schema-registry:8081",
2020
"tasks.max": "1",
21-
"topics": "redis.commands.set,redis.commands.expire,redis.commands.expireat,redis.commands.pexpire,redis.commands.sadd,redis.commands.geoadd",
21+
"topics": "redis.commands.set,redis.commands.expire,redis.commands.expireat,redis.commands.pexpire,redis.commands.sadd,redis.commands.geoadd,redis.commands.arbitrary",
2222
"redis.uri": "redis://IEPfIr0eLF7UsfwrIlzy80yUaBG258j9@redis-cluster",
2323
"redis.cluster.enabled": true
2424
}
@@ -112,6 +112,17 @@ kafka-avro-console-producer \
112112
>{"key":"Sicily","values":[{"longitude":13.361389,"latitude":13.361389,"member":"Palermo"},{"longitude":15.087269,"latitude":37.502669,"member":"Catania"}]}
113113
```
114114

115+
```bash
116+
kafka-avro-console-producer \
117+
--broker-list kafka-broker-0.kafka-broker:9092 \
118+
--property schema.registry.url='http://kafka-schema-registry:8081' \
119+
--property value.schema='{"namespace":"io.github.jaredpetersen.kafkaconnectredis","name":"RedisArbitraryCommand","type":"record","fields":[{"name":"command","type":"string"},{"name":"arguments","type":{"type":"array","items":"string"}}]}' \
120+
--topic redis.commands.arbitrary
121+
>{"command":"TS.CREATE","arguments":["temperature:3:11", "RETENTION", "60", "LABELS", "sensor_id", "2", "area_id", "32"]}
122+
>{"command":"TS.ADD","arguments":["temperature:3:11", "1548149181", "30"]}
123+
>{"command":"TS.ADD","arguments":["temperature:3:11", "1548149191", "42"]}
124+
```
125+
115126
### Connect JSON
116127
Create an interactive ephemeral query pod:
117128
```bash
@@ -134,6 +145,9 @@ kafka-console-producer \
134145
>{"payload":{"key":"{user.1}.interests","values":["reading"]},"schema":{"name":"io.github.jaredpetersen.kafkaconnectredis.RedisSaddCommand","type":"struct","fields":[{"field":"key","type":"string","optional":false},{"field":"values","type":"array","items":{"type":"string"},"optional":false}]}}
135146
>{"payload":{"key":"{user.2}.interests","values":["sailing","woodworking","programming"]},"schema":{"name":"io.github.jaredpetersen.kafkaconnectredis.RedisSaddCommand","type":"struct","fields":[{"field":"key","type":"string","optional":false},{"field":"values","type":"array","items":{"type":"string"},"optional":false}]}}
136147
>{"payload":{"key":"Sicily","values":[{"longitude":13.361389,"latitude":13.361389,"member":"Palermo"},{"longitude":15.087269,"latitude":37.502669,"member":"Catania"}]},"schema":{"name":"io.github.jaredpetersen.kafkaconnectredis.RedisGeoaddCommand","type":"struct","fields":[{"field":"key","type":"string","optional":false},{"field":"values","type":"array","items":{"type":"struct","fields":[{"field":"longitude","type":"double","optional":false},{"field":"latitude","type":"double","optional":false},{"field":"member","type":"string","optional":false}]},"optional":false}]}}
148+
>{"payload":{"command":"TS.CREATE","arguments":["temperature:3:11", "RETENTION", "60", "LABELS", "sensor_id", "2", "area_id", "32"]},"schema":{"name":"io.github.jaredpetersen.kafkaconnectredis.RedisArbitraryCommand","type":"struct","fields":[{"field":"command","type":"string","optional":false},{"field":"arguments","type":"array","items":{"type":"string"},"optional":false}]}}
149+
>{"payload":{"command":"TS.ADD","arguments":["temperature:3:11", "1548149181", "30"]},"schema":{"name":"io.github.jaredpetersen.kafkaconnectredis.RedisArbitraryCommand","type":"struct","fields":[{"field":"command","type":"string","optional":false},{"field":"arguments","type":"array","items":{"type":"string"},"optional":false}]}}
150+
>{"payload":{"command":"TS.ADD","arguments":["temperature:3:11", "1548149191", "42"]},"schema":{"name":"io.github.jaredpetersen.kafkaconnectredis.RedisArbitraryCommand","type":"struct","fields":[{"field":"command","type":"string","optional":false},{"field":"arguments","type":"array","items":{"type":"string"},"optional":false}]}}
137151
```
138152

139153
## Validate
@@ -160,4 +174,5 @@ PTTL product.waffles
160174
SMEMBERS {user.1}.interests
161175
SMEMBERS {user.2}.interests
162176
GEOPOS Sicily Catania
177+
TS.RANGE temperature:3:11 1548149180 1548149210 AGGREGATION avg 5
163178
```

docs/demo/docker/redis/Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM redislabs/redistimeseries:1.4.6 as redistimeseries
2+
FROM redis:6
3+
4+
ENV LIBRARY_PATH /usr/lib/redis/modules
5+
6+
COPY --from=redistimeseries ${LIBRARY_PATH}/redistimeseries.so ${LIBRARY_PATH}/redistimeseries.so
7+
8+
ENTRYPOINT ["redis-server"]
9+
CMD ["/usr/local/etc/redis/redis.conf", "--loadmodule", "/usr/lib/redis/modules/redistimeseries.so"]

docs/demo/kubernetes/redis/statefulset.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ spec:
1616
spec:
1717
containers:
1818
- name: redis
19-
image: redis:6
20-
args:
21-
- /usr/local/etc/redis/redis.conf
19+
image: jaredpetersen/redis:latest
20+
imagePullPolicy: Never
2221
ports:
2322
- containerPort: 6379
2423
name: client

pom.xml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>io.github.jaredpetersen</groupId>
77
<artifactId>kafka-connect-redis</artifactId>
8-
<version>1.0.3</version>
8+
<version>1.0.4</version>
99
<packaging>jar</packaging>
1010

1111
<name>kafka-connect-redis</name>
@@ -69,7 +69,7 @@
6969
<dependency>
7070
<groupId>io.lettuce</groupId>
7171
<artifactId>lettuce-core</artifactId>
72-
<version>6.0.0.RELEASE</version>
72+
<version>6.0.1.RELEASE</version>
7373
</dependency>
7474

7575
<dependency>
@@ -164,7 +164,8 @@
164164
<configuration>
165165
<configLocation>checkstyle.xml</configLocation>
166166
<consoleOutput>true</consoleOutput>
167-
<failsOnError>true</failsOnError>
167+
<failOnViolation>true</failOnViolation>
168+
<violationSeverity>warning</violationSeverity>
168169
<includeTestSourceDirectory>true</includeTestSourceDirectory>
169170
<suppressionsLocation>checkstyle-test-suppressions.xml</suppressionsLocation>
170171
</configuration>
@@ -179,13 +180,6 @@
179180
<execution>
180181
<id>validate</id>
181182
<phase>validate</phase>
182-
<configuration>
183-
<configLocation>checkstyle.xml</configLocation>
184-
<consoleOutput>true</consoleOutput>
185-
<failsOnError>true</failsOnError>
186-
<violationSeverity>warning</violationSeverity>
187-
<includeTestSourceDirectory>true</includeTestSourceDirectory>
188-
</configuration>
189183
<goals>
190184
<goal>check</goal>
191185
</goals>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.github.jaredpetersen.kafkaconnectredis.sink.writer;
2+
3+
import io.lettuce.core.codec.RedisCodec;
4+
import io.lettuce.core.output.CommandOutput;
5+
import java.nio.ByteBuffer;
6+
7+
/**
8+
* Void output of a Redis command used to "fire and forget".
9+
* <p />
10+
* Temporary stopgap until https://github.com/lettuce-io/lettuce-core/issues/1529 is released.
11+
*
12+
* @param <K> Key type.
13+
* @param <V> Value type.
14+
*/
15+
class LettuceVoidOutput<K, V> extends CommandOutput<K, V, Void> {
16+
public LettuceVoidOutput(RedisCodec<K, V> codec) {
17+
super(codec, null);
18+
}
19+
20+
@Override
21+
public void set(ByteBuffer bytes) {
22+
}
23+
24+
@Override
25+
public void set(long integer) {
26+
}
27+
28+
@Override
29+
public void set(double number) {
30+
}
31+
32+
@Override
33+
public void set(boolean value) {
34+
}
35+
}

src/main/java/io/github/jaredpetersen/kafkaconnectredis/sink/writer/RecordConverter.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.jaredpetersen.kafkaconnectredis.sink.writer;
22

3+
import io.github.jaredpetersen.kafkaconnectredis.sink.writer.record.RedisArbitraryCommand;
34
import io.github.jaredpetersen.kafkaconnectredis.sink.writer.record.RedisCommand;
45
import io.github.jaredpetersen.kafkaconnectredis.sink.writer.record.RedisExpireCommand;
56
import io.github.jaredpetersen.kafkaconnectredis.sink.writer.record.RedisExpireatCommand;
@@ -51,6 +52,9 @@ public Mono<RedisCommand> convert(SinkRecord sinkRecord) {
5152
case "io.github.jaredpetersen.kafkaconnectredis.RedisGeoaddCommand":
5253
redisCommandMono = convertGeoadd(recordValue);
5354
break;
55+
case "io.github.jaredpetersen.kafkaconnectredis.RedisArbitraryCommand":
56+
redisCommandMono = convertArbitrary(recordValue);
57+
break;
5458
default:
5559
redisCommandMono = Mono.error(new ConnectException("unsupported command schema " + recordValueSchemaName));
5660
}
@@ -162,4 +166,17 @@ private Mono<RedisCommand> convertGeoadd(Struct value) {
162166
.build();
163167
}));
164168
}
169+
170+
private Mono<RedisCommand> convertArbitrary(Struct value) {
171+
return Mono.fromCallable(() -> {
172+
final RedisArbitraryCommand.Payload payload = RedisArbitraryCommand.Payload.builder()
173+
.command(value.getString("command"))
174+
.arguments(value.getArray("arguments"))
175+
.build();
176+
177+
return RedisArbitraryCommand.builder()
178+
.payload(payload)
179+
.build();
180+
});
181+
}
165182
}

src/main/java/io/github/jaredpetersen/kafkaconnectredis/sink/writer/Writer.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.jaredpetersen.kafkaconnectredis.sink.writer;
22

3+
import io.github.jaredpetersen.kafkaconnectredis.sink.writer.record.RedisArbitraryCommand;
34
import io.github.jaredpetersen.kafkaconnectredis.sink.writer.record.RedisCommand;
45
import io.github.jaredpetersen.kafkaconnectredis.sink.writer.record.RedisExpireCommand;
56
import io.github.jaredpetersen.kafkaconnectredis.sink.writer.record.RedisExpireatCommand;
@@ -10,6 +11,11 @@
1011
import io.lettuce.core.SetArgs;
1112
import io.lettuce.core.api.reactive.RedisReactiveCommands;
1213
import io.lettuce.core.cluster.api.reactive.RedisClusterReactiveCommands;
14+
import io.lettuce.core.codec.StringCodec;
15+
import io.lettuce.core.output.CommandOutput;
16+
import io.lettuce.core.protocol.CommandArgs;
17+
import io.lettuce.core.protocol.ProtocolKeyword;
18+
import java.nio.charset.StandardCharsets;
1319
import java.util.Arrays;
1420
import org.apache.kafka.connect.errors.ConnectException;
1521
import org.slf4j.Logger;
@@ -76,6 +82,9 @@ public Mono<Void> write(RedisCommand redisCommand) {
7682
case GEOADD:
7783
response = geoadd((RedisGeoaddCommand) redisCommand);
7884
break;
85+
case ARBITRARY:
86+
response = arbitrary((RedisArbitraryCommand) redisCommand);
87+
break;
7988
default:
8089
response = Mono.error(new ConnectException("redis command " + redisCommand + " is not supported"));
8190
}
@@ -182,4 +191,28 @@ private Mono<Void> geoadd(RedisGeoaddCommand geoaddCommand) {
182191

183192
return geoaddResult.then();
184193
}
194+
195+
private Mono<Void> arbitrary(RedisArbitraryCommand arbitraryCommand) {
196+
// Set up arbitrary command
197+
final ProtocolKeyword protocolKeyword = new ProtocolKeyword() {
198+
public final byte[] bytes = name().getBytes(StandardCharsets.US_ASCII);
199+
200+
@Override
201+
public byte[] getBytes() {
202+
return this.bytes;
203+
}
204+
205+
@Override
206+
public String name() {
207+
return arbitraryCommand.getPayload().getCommand();
208+
}
209+
};
210+
final CommandOutput<String, String, Void> commandOutput = new LettuceVoidOutput<>(StringCodec.UTF8);
211+
final CommandArgs<String, String> commandArgs = new CommandArgs<>(StringCodec.UTF8)
212+
.addValues(arbitraryCommand.getPayload().getArguments());
213+
214+
return (this.clusterEnabled)
215+
? this.redisClusterCommands.dispatch(protocolKeyword, commandOutput, commandArgs).then()
216+
: this.redisStandaloneCommands.dispatch(protocolKeyword, commandOutput, commandArgs).then();
217+
}
185218
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.github.jaredpetersen.kafkaconnectredis.sink.writer.record;
2+
3+
import java.util.List;
4+
import lombok.Builder;
5+
import lombok.Value;
6+
7+
@Value
8+
@Builder(builderClassName = "Builder")
9+
public class RedisArbitraryCommand implements RedisCommand {
10+
Command command = Command.ARBITRARY;
11+
Payload payload;
12+
13+
@Value
14+
@lombok.Builder(builderClassName = "Builder")
15+
public static class Payload {
16+
String command;
17+
List<String> arguments;
18+
}
19+
}

src/main/java/io/github/jaredpetersen/kafkaconnectredis/sink/writer/record/RedisCommand.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
public interface RedisCommand {
44
enum Command {
5+
ARBITRARY,
56
SET,
67
EXPIRE,
78
EXPIREAT,

src/main/java/io/github/jaredpetersen/kafkaconnectredis/source/config/RedisSourceConfig.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.github.jaredpetersen.kafkaconnectredis.source.config;
22

3-
import java.util.Collections;
43
import java.util.List;
54
import java.util.Map;
65
import org.apache.kafka.common.config.AbstractConfig;

0 commit comments

Comments
 (0)