Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions VotingPlugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<version>3.6.1</version>
<executions>
<execution>
<id>default-test</id>
Expand Down Expand Up @@ -408,7 +408,7 @@
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.6</version>
<version>2.7</version>
</dependency>
<dependency>
<groupId>org.spigotmc</groupId>
Expand All @@ -431,7 +431,7 @@
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.14.0</version>
<version>2.15.0</version>
<scope>provided</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -482,7 +482,7 @@
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>8.0.1</version>
<version>8.0.2</version>
</dependency>
<dependency>
<groupId>me.clip</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package com.bencodez.votingplugin.tests.packaging;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarFile;
import javax.tools.ToolProvider;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
Expand Down Expand Up @@ -119,6 +123,125 @@ void packagedRedisClientLinksWithModuleApis() throws Exception {
}
}

@Test
void packagedJedisUsesTheResolvedClientRatherThanAnEmbeddedProducerCopy() throws Exception {
Path selected = Path.of(redis.clients.jedis.Jedis.class.getProtectionDomain()
.getCodeSource().getLocation().toURI());
try (JarFile dependency = new JarFile(selected.toFile());
JarFile artifact = new JarFile(packagedJar().toFile())) {
// MultiDbClient differs across the 8.0.1/8.0.2 patch boundary; Jedis alone does not.
for (String name : java.util.List.of("redis/clients/jedis/Jedis.class",
"redis/clients/jedis/JedisPool.class", "redis/clients/jedis/MultiDbClient.class")) {
assertNotNull(dependency.getEntry(name), "Missing resolved client API: " + name);
assertNotNull(artifact.getEntry(name), "Missing packaged client API: " + name);
assertEquals(1L, artifact.stream().filter(entry -> entry.getName().equals(name)).count(),
"Duplicate packaged client: " + name);
try (var expected = dependency.getInputStream(dependency.getEntry(name));
var actual = artifact.getInputStream(artifact.getEntry(name))) {
assertArrayEquals(expected.readAllBytes(), actual.readAllBytes(),
"A producer's embedded client must not shadow the resolved dependency: " + name);
}
}
}
}

@Test
void packagedSimpleApiRedisHandlerUsesTheSelectedClientWithoutMavenDependencies(@TempDir Path directory)
throws Exception {
assertRedisHandlerLinks(packagedJar(),
"com.bencodez.votingplugin.simpleapi.servercomm.redis.RedisHandler", directory);
}

@Test
void publishedProducerArtifactsRetainLegacyRedisApisAndLinkTheirHandlers(@TempDir Path directory) throws Exception {
var resources = PackagedArtifactTest.class.getClassLoader()
.getResources("com/bencodez/simpleapi/servercomm/redis/RedisHandler.class");
java.util.Set<URL> producers = new java.util.LinkedHashSet<>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
assertEquals("jar", resource.getProtocol(), "Producer verification requires actual dependency JARs");
var connection = (java.net.JarURLConnection) resource.openConnection();
connection.setUseCaches(false);
producers.add(connection.getJarFileURL());
}
assertEquals(2, producers.size(), "Verify both the normal AdvancedCore and direct SimpleAPI artifacts");
int index = 0;
URL platformSlf4j = org.slf4j.Logger.class.getProtectionDomain().getCodeSource().getLocation();
for (URL producer : producers) {
try (URLClassLoader loader = new URLClassLoader(new URL[] { producer, platformSlf4j },
ClassLoader.getPlatformClassLoader())) {
Class<?> endpoint = Class.forName("redis.clients.jedis.HostAndPort", true, loader);
Class<?> config = Class.forName("redis.clients.jedis.JedisClientConfig", true, loader);
Class.forName("redis.clients.jedis.JedisPooled", true, loader).getConstructor(endpoint, config);
Class.forName("redis.clients.jedis.JedisSentineled", true, loader)
.getConstructor(String.class, config, java.util.Set.class, config);
Class.forName("redis.clients.jedis.UnifiedJedis", true, loader).getConstructor(endpoint, config);
}
assertRedisHandlerLinks(Path.of(producer.toURI()), "com.bencodez.simpleapi.servercomm.redis.RedisHandler",
Files.createDirectory(directory.resolve("producer-" + index++)));
}
}

private static void assertRedisHandlerLinks(Path artifact, String handlerName, Path directory) throws Exception {
Path source = directory.resolve("PackagedRedisHandlerProbe.java");
Files.writeString(source, """
public final class PackagedRedisHandlerProbe
extends %s {
public PackagedRedisHandlerProbe(boolean tls) {
super("localhost", 0, "", "", 3, tls);
}
@Override public void debug(String message) { }
public Object listener() {
return createEnvelopeListener("synthetic", (channel, envelope) -> { });
}
}
""".formatted(handlerName));
var compiler = ToolProvider.getSystemJavaCompiler();
assertNotNull(compiler);
StringWriter diagnostics = new StringWriter();
try (var files = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) {
assertTrue(compiler.getTask(diagnostics, files, null,
java.util.List.of("--release", "21", "-proc:none", "-classpath", artifact.toString(),
"-d", directory.toString()), null,
files.getJavaFileObjects(source.toFile())).call(), diagnostics.toString());
}
URL platformSlf4j = org.slf4j.Logger.class.getProtectionDomain().getCodeSource().getLocation();
try (URLClassLoader loader = new URLClassLoader(new URL[] {
directory.toUri().toURL(), artifact.toUri().toURL(), platformSlf4j },
ClassLoader.getPlatformClassLoader())) {
Class<?> probe = Class.forName("PackagedRedisHandlerProbe", true, loader);
Class<?> handler = probe.getSuperclass();
Class<?> configApi = Class.forName("redis.clients.jedis.JedisClientConfig", true, loader);
Class<?> subscriber = Class.forName("redis.clients.jedis.JedisPubSub", true, loader);
Class<?> jedis = Class.forName("redis.clients.jedis.Jedis", true, loader);
jedis.getMethod("publish", String.class, String.class);
jedis.getMethod("subscribe", subscriber, String[].class);
var configField = handler.getDeclaredField("clientConfig");
configField.setAccessible(true);
var poolField = handler.getDeclaredField("publisherPool");
poolField.setAccessible(true);
for (boolean tls : new boolean[] { false, true }) {
Object instance = probe.getConstructor(boolean.class).newInstance(tls);
Object pool = poolField.get(instance);
try {
Object config = configField.get(instance);
assertEquals(tls, configApi.getMethod("isSsl").invoke(config));
assertEquals(3, configApi.getMethod("getDatabase").invoke(config));
if (tls) {
var parameters = (javax.net.ssl.SSLParameters) configApi.getMethod("getSslParameters").invoke(config);
assertEquals("HTTPS", parameters.getEndpointIdentificationAlgorithm());
}
assertTrue(subscriber.isInstance(probe.getMethod("listener").invoke(instance)));
assertEquals(false, pool.getClass().getMethod("isClosed").invoke(pool));
} finally {
// Constructors are lazy: no publish, subscribe or connection borrow is performed.
handler.getMethod("close").invoke(instance);
}
assertEquals(true, pool.getClass().getMethod("isClosed").invoke(pool));
}
}
}

@Test
void packagedJdkTlsIdentityWorksWithoutExternalCrypto(@TempDir Path directory) throws Exception {
URL jar = packagedJar().toUri().toURL();
Expand Down
11 changes: 11 additions & 0 deletions docs/redis-dependency-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Redis dependency compatibility

The dependency versions in SimpleAPI, AdvancedCore and VotingPlugin describe different packaging boundaries. Equal version strings alone do not prove that a consumer can link or that a shaded JAR contains the intended implementation.

- SimpleAPI's full artifact preserves the established Jedis 7 API, including `JedisPooled`, `JedisSentineled` and the public `UnifiedJedis` constructors. Its existing full-artifact test compiles and links a legacy consumer using only that JAR. Jedis 8 removes those APIs; replacing the producer dependency without a migration would break the full-artifact contract.
- AdvancedCore excludes the ordinary transitive Jedis dependency from SimpleAPI. This exclusion does not strip classes already embedded in SimpleAPI's full JAR. The published AdvancedCore artifact therefore still contains that producer's Redis implementation.
- VotingPlugin filters `redis/clients/**` from both producer JARs and explicitly selects its own Jedis client. These filters must remain together: removing only a Maven transitive dependency does not prevent a fat producer JAR from shadowing the selected client.

VotingPlugin's package checks verify the selected implementation's class bytes, including an anchor that distinguishes the 8.0.1 and 8.0.2 patch artifacts. They compile and load the relocated SimpleAPI Redis handler using only the downloadable plugin JAR and the platform logging API. The same constructor, listener, TLS configuration and close checks run separately against the actual normal AdvancedCore and SimpleAPI dependency JARs; their legacy constructors remain checked in isolated class loaders. Maven's test dependency classpath cannot supply a missing Redis class to these probes.

Keep the SimpleAPI legacy exclusion until a separately reviewed compatibility-preserving migration exists. Evaluate each dependency update against its actual public API, supported Java version, resolved artifacts and downstream packaged linkage. A passed constructor/linkage check is not a Redis broker integration test: the probes do not publish, subscribe, borrow a connection or start a service. Native and live transport acceptance remain separate.
Loading