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
77 changes: 76 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ After changing `roda-core`, run `mvn install -Pcore -DskipTests` before building

1. **GitHub Packages auth is required.** Maven build will fail without a valid `~/.m2/settings.xml` with a GitHub PAT having `read:packages`.

2. **Always start Docker services before running tests.** Tests are integration tests — they need live Solr, PostgreSQL, ZooKeeper, and LDAP.
2. **Always start Docker services before running tests.** Tests use Testcontainers (auto-starts ZooKeeper, Solr, PostgreSQL, LDAP, Mailpit, ClamAV, Siegfried). The Docker daemon must be running. In the Claude Code cloud environment, start it with: `service docker start` (may print an ulimit warning — that is harmless).

3. **Use the correct Maven profile.**
- Skip UI/GWT: use `-Pcore`
Expand All @@ -436,3 +436,78 @@ After changing `roda-core`, run `mvn install -Pcore -DskipTests` before building
9. **PREMIS metadata is mandatory.** Every preservation action must record a PREMIS event in the AIP's metadata. Follow existing plugin implementations as examples.

10. **Commit signing.** Commits should be GPG-signed per the project's contribution guidelines. See: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits

---

## Claude Code Cloud Environment — Quick Reference

This section captures environment-specific quirks for running in the Claude Code remote container.

### Docker Setup

Docker daemon is installed but may not be running at session start:

```bash
# Check if Docker is running
docker ps

# If not running, start it (the ulimit warning is harmless):
service docker start

# Verify
docker ps # should show empty table, not an error
```

### Build Commands (Cloud Environment)

Maven Central access may be blocked by a proxy. Always use **offline mode** (`-o`) or the local repo when possible. The proxy is pre-configured via `JAVA_TOOL_OPTIONS` env var.

```bash
# Step 1: Build and install core modules (no tests, no GWT)
mvn install -Pcore -DskipTests -Denforcer.skip=true

# Step 2: Run a single test class to verify (fast validation)
mvn -pl roda-core/roda-core-tests test -Pcore \
-Dtestng.groups="travis" \
-Denforcer.skip=true \
-Dsurefire.suiteXmlFiles=testng-single.xml \
-o

# Step 3: Run full CI test suite
mvn -Pcore -Dtestng.groups="travis" -Denforcer.skip=true \
clean org.jacoco:jacoco-maven-plugin:prepare-agent test
```

### Single-Test Shortcut

`roda-core/roda-core-tests/testng-single.xml` targets only `IndexServiceTest`. Edit the `<class name="...">` element to point at any test class you want to run in isolation.

### Test Infrastructure (Testcontainers)

Tests use `TestContainersManager` (singleton) to start containers once per JVM. The `RodaContainersLifecycleListener` triggers it via `testng.xml`. Containers started:

| Service | Image |
|-------------|---------------------------|
| ZooKeeper | zookeeper:3.9.1-jre-17 |
| Solr | solr:9 |
| PostgreSQL | postgres:17 |
| Mailpit | axllent/mailpit:latest |
| ClamAV | clamav/clamav:1.5.2 |
| Siegfried | keeps/siegfried:v1.11.0 |

**Important**: On Linux, Solr registers its container IP in ZooKeeper. Bridge network IPs (`172.x.x.x`) are directly routable from the host — no port mapping is needed for the CloudSolrClient to reach Solr live nodes.

### ZooKeeper / Solr Connection Notes

- `zkConnectTimeout` defaults to 15 s in SolrJ. If the ZK session is not established within that window, `SolrZkClient` calls `ZooKeeper.close()`, which **hangs indefinitely** (sends CLOSESESSION but has no threads left to receive the response).
- The fix is in `RodaCoreFactory.instantiateSolr()`: `withZkConnectTimeout(300000, MILLISECONDS)` is set on the builder.
- `TestContainersManager` also sets `System.setProperty("zkConnectTimeout", "300000")` as belt-and-suspenders.

### Pre-PR Checklist

Before pushing/creating a PR:
1. `service docker start` (if not already running)
2. `mvn install -Pcore -DskipTests -Denforcer.skip=true` — compile all modules
3. Run a targeted single test to validate the change area
4. Run full CI test suite if the change is broad
5. Verify no Checkstyle violations (they are enforced in CI)
2 changes: 1 addition & 1 deletion roda-core/roda-core-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-Droda.environment.collect.version=false</argLine>
<argLine>-Droda.environment.collect.version=false -Dhttp.nonProxyHosts=localhost|127.0.0.1|172.*|192.*|169.254.169.254|metadata.google.internal|*.svc.cluster.local|*.local|*.googleapis.com|*.google.com</argLine>
<useSystemClassLoader>true</useSystemClassLoader>
<useManifestOnlyJar>false</useManifestOnlyJar>
<suiteXmlFiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ private TestContainersManager() {
// ZooKeeper — exposed so that the RODA CloudSolrClient can connect
zookeeper = new GenericContainer<>(DockerImageName.parse("zookeeper:3.9.1-jre-17")).withNetwork(network)
.withNetworkAliases("zookeeper").withExposedPorts(2181)
.withEnv("ZOO_TICK_TIME", "10000")
.withEnv("ZOO_CFG_EXTRA", "maxSessionTimeout=600000")
.waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(60)));
zookeeper.start();
LOGGER.info("ZooKeeper started at {}:{}", zookeeper.getHost(), zookeeper.getMappedPort(2181));
Expand All @@ -66,9 +68,16 @@ private TestContainersManager() {
// the container's bridge-network IP. On Linux (CI and most developer
// machines) this IP is directly reachable from the Docker host, so the
// CloudSolrClient can connect without any additional port mapping.
//
// Wait for the log message that Solr emits immediately after registering
// the live node in ZooKeeper. This log fires AFTER ZkController.registerLiveNode(),
// so by the time this wait strategy succeeds, the live node is already
// present in ZooKeeper and RodaCoreFactory.connect() will find it instantly.
// Using a log-based strategy avoids any HTTP proxy interference.
solr = new GenericContainer<>(DockerImageName.parse("solr:9")).withNetwork(network)
.withEnv("ZK_HOST", "zookeeper:2181").withExposedPorts(8983).waitingFor(Wait.forHttp("/solr/admin/info/system")
.forPort(8983).forStatusCode(200).withStartupTimeout(Duration.ofMinutes(3)));
.withEnv("ZK_HOST", "zookeeper:2181").withExposedPorts(8983)
.waitingFor(Wait.forLogMessage(".*Register node as live in ZooKeeper.*", 1)
.withStartupTimeout(Duration.ofMinutes(3)));
solr.start();
LOGGER.info("Solr started at {}:{}", solr.getHost(), solr.getMappedPort(8983));

Expand Down Expand Up @@ -145,6 +154,15 @@ private void configureSystemProperties() {
System.setProperty("RODA_CORE_EMAIL_HOST", mailpit.getHost());
System.setProperty("RODA_CORE_EMAIL_PORT", mailpit.getMappedPort(1025).toString());

// Give Solr Cloud more time to establish its ZooKeeper connection in
// environments where ZkClient session establishment is slow.
System.setProperty("RODA_CORE_SOLR_CLOUD_CONNECT_TIMEOUT_MS", "300000");
// Increase ZK connect timeout so SolrZkClient does not call ZooKeeper.close()
// before the session is established (the close() hangs indefinitely when there
// are no background ZK threads left to process the CLOSESESSION response).
System.setProperty("RODA_CORE_SOLR_CLOUD_ZK_CONNECT_TIMEOUT_MS", "300000");
System.setProperty("zkConnectTimeout", "300000");

System.setProperty("RODA_CORE_PLUGINS_INTERNAL_VIRUS_CHECK_CLAMAV_PARAMS", "-m --stream -c /tmp/clamd.conf");

String siegfriedUrl = "http://" + siegfried.getHost() + ":" + siegfried.getMappedPort(5138);
Expand Down
12 changes: 12 additions & 0 deletions roda-core/roda-core-tests/testng-single.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="SingleTest" verbose="1">
<listeners>
<listener class-name="org.roda.core.RodaContainersLifecycleListener" />
</listeners>
<test name="single" preserve-order="true">
<classes>
<class name="org.roda.core.index.IndexServiceTest" />
</classes>
</test>
</suite>
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,11 @@ private static SolrClient instantiateSolr(Path solrHome, boolean writeIsAllowed)
zkChroot = Optional.empty();
}

CloudSolrClient cloudSolrClient = new CloudSolrClient.Builder(zkHosts, zkChroot).build();
int zkClientTimeout = getRodaConfiguration().getInt("core.solr.cloud.zk.client.timeout_ms", 600000);
int zkConnectTimeout = getRodaConfiguration().getInt("core.solr.cloud.zk.connect.timeout_ms", 300000);
CloudSolrClient cloudSolrClient = new CloudSolrClient.Builder(zkHosts, zkChroot)
.withZkClientTimeout(zkClientTimeout, TimeUnit.MILLISECONDS)
.withZkConnectTimeout(zkConnectTimeout, TimeUnit.MILLISECONDS).build();

waitForSolrCluster(cloudSolrClient);

Expand Down Expand Up @@ -1089,7 +1093,8 @@ private static boolean checkSolrCluster(CloudSolrClient cloudSolrClient)
cloudSolrClient.connect(connectTimeout, TimeUnit.MILLISECONDS);
LOGGER.info("Connected to Solr Cloud");
} catch (TimeoutException e) {
throw new GenericException("Could not connect to Solr Cloud", e);
LOGGER.warn("Timed out waiting for Solr Cloud live nodes (will retry): {}", e.getMessage());
return false;
}

ClusterState clusterState = cloudSolrClient.getClusterState();
Expand Down Expand Up @@ -1418,7 +1423,7 @@ private static void initializeLdapServer(NodeType nodeType) {
private static void indexUsersAndGroupsFromLDAP() throws GenericException {
for (User user : getModelService().listUsers()) {
getModelService().notifyUserUpdated(user).failOnError();
if (INSTANTIATE_SOLR) {
if (INSTANTIATE_SOLR && getIndexService() != null) {
try {
PremisV3Utils.createOrUpdatePremisUserAgentBinary(user.getName(), getModelService(), getIndexService(), true);
} catch (ValidationException | NotFoundException | RequestNotValidException | AuthorizationDeniedException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ core.storage.type=FILESYSTEM
##########################################################################
core.solr.type=CLOUD
core.solr.cloud.urls=localhost:2181
core.solr.cloud.connect.timeout_ms=60000
core.solr.cloud.connect.timeout_ms=300000
core.solr.cloud.healthcheck.retries=100
core.solr.cloud.healthcheck.timeout_ms=10000
core.solr.cloud.zk.client.timeout_ms=600000

# Stemming and stopwords configuration for "*_txt" fields
# When missing or blank Solr uses the "text_general" type for "*_txt"
Expand Down
Loading