Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependency_tree.txt
/.local-notes/
itests/snapshots_repository/
itests/archives/
itests/.test-timing-cache-*.properties
.env.local
.venv*
javadoc_*
5 changes: 0 additions & 5 deletions .mvn/extensions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@

-->
<extensions>
<extension>
<groupId>org.apache.maven.extensions</groupId>
<artifactId>maven-build-cache-extension</artifactId>
<version>1.2.1</version>
</extension>
<extension>
<groupId>com.gradle</groupId>
<artifactId>develocity-maven-extension</artifactId>
Expand Down
43 changes: 0 additions & 43 deletions .mvn/maven-build-cache-config.xml

This file was deleted.

108 changes: 108 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<!--
SPDX-License-Identifier: Apache-2.0

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.
-->

# Apache Unomi — CLAUDE.md

Apache Unomi is a Java/OSGi customer data platform running on Apache Karaf. Multi-module Maven
reactor; most runtime code is packaged as OSGi bundles wired via Blueprint XML
(`OSGI-INF/blueprint/blueprint.xml` in each module), not Spring/CDI.

## Dual persistence backends — the #1 thing to know

Every persistence-related feature must be implemented **twice**, once per search engine, and the
two implementations must behave identically:

- `persistence-elasticsearch/core` — `ElasticSearchPersistenceServiceImpl.java` (Elasticsearch, ILM
for rollover, `co.elastic.clients.elasticsearch.*` client)
- `persistence-opensearch/core` — `OpenSearchPersistenceServiceImpl.java` (OpenSearch, ISM for
rollover, `org.opensearch.client.opensearch.*` client)

The two client libraries look similar (OpenSearch's Java client is a fork of Elastic's) but their
typed builder APIs differ in real, non-obvious ways — e.g. ES's `numberOfShards(String)` vs
OpenSearch's `numberOfShards(Integer)`. **Do not port code between the two by pattern-matching
alone — verify actual method signatures for the specific client version in use** (`mvn
dependency:tree` + decompile if needed; there is no live cluster available in most dev sandboxes to
test against directly).

ISM (OpenSearch) and ILM (ES) are conceptually parallel but not identical: ISM policies attach to
indices either via an explicit `plugins.index_state_management.policy_id` index setting or via the
policy's own `ism_template.index_patterns` matching at index-creation time — prefer the explicit
`policy_id` setting (set via `IndexSettings.customSettings(...)`, since there's no typed builder
field for it) over template pattern-matching, which is fragile (a past bug: the pattern list was
built from raw item-type names like `"event"` that never matched real index names like
`"context-event-000001"`).

**Composable index template priority**: `BaseIT` registers an IT-only catch-all template
(`unomi-it-zero-replicas`, pattern `*`, priority `1`) to force zero replicas on single-node test
clusters. Any real Unomi index template must use a priority well above that (convention: `100`) or
template registration fails outright — composable templates reject ambiguous same-priority
overlapping patterns rather than merging them.

**Single-node test clusters legitimately stay `yellow` forever** (no second node to host replicas).
Code that blocks on `HealthStatus.Green` will hang/fail in ITs. OpenSearch's persistence service has
a `minimalClusterState` config + `getHealthStatus(minimalClusterState)` helper for this — reuse it.
**Elasticsearch's persistence service has no equivalent wired in** (only the separate
`extensions/healthcheck` bundle knows about yellow-tolerance) — this is a known, unaddressed parity
gap, not an oversight to silently "fix" by inventing new config without discussing it with the user
first.

## Build

- Root `pom.xml` aggregates ~40 modules. Key ones: `api`, `common`, `persistence-spi`,
`persistence-elasticsearch`, `persistence-opensearch`, `services`, `rest`, `graphql`, `wab`,
`kar` (Karaf feature repo), `itests`.
- `./build.sh` is the canonical local build entrypoint (aligns with CI). Prefer it over ad hoc
`mvn` invocations when validating a full build.
- The `itests` module only joins the reactor under the `integration-tests` (or `ci-build-itests`)
profile — `mvn -pl itests ...` alone will fail with "module not found in reactor" unless that
profile is active.
- Karaf `karaf-maven-plugin:verify` failures on unrelated modules (e.g. stale
`target/classes` "(Is a directory)" errors) are usually stale local build state, not real
regressions — don't chase them as if they were caused by your change without checking first.

## Integration tests (`itests/`)

- PaxExam + Karaf, `PerSuite` reactor strategy: **one shared Karaf container/cluster runs the
entire suite**, not one per test class. Tests must clean up after themselves (`@After`) or they
pollute later tests' assumptions.
- **`itests/pom.xml`'s failsafe config only includes `**/*AllITs.java`** — individual `*IT.java`
classes are silently never run unless explicitly registered in `AllITs.java`'s `@SuiteClasses`
list. This is easy to forget when adding a new IT.
- `BaseIT.java` is the shared base class: search-engine-agnostic HTTP helpers
(`createSearchEngineHttpClient()`, `getSearchEngineBaseUrl()`), `keepTrying(...)` for
poll-with-timeout assertions, and Karaf container config (`etc/custom.system.properties`
overrides) shared by ES and OpenSearch runs. IT-only config differences between the two engines
(e.g. `rollover.maxDocs`) must be set for **both** `org.apache.unomi.elasticsearch.*` and
`org.apache.unomi.opensearch.*` properties, or one engine silently runs untested.
- `itests/kt.sh` is a helper for driving/inspecting a running IT Karaf instance (`kt.sh t` to run,
`kt.sh l` to view the log) — see that script and any `itests/README*` for the current test-dev
workflow rather than assuming.
- Run engine-specific ITs via the search-engine system property; check `BaseIT` /
`AllITs`/`build.sh` for the exact flag rather than guessing.

## Working with subagents / cost awareness

Verifying an unfamiliar client library's exact API surface via jar decompilation (`javap`) is a
legitimate but expensive way to work when no live test environment is available — it burns a lot of
tool calls. When a fix's correctness can't be confirmed without actually running the code (e.g.
against a live Elasticsearch/OpenSearch container), prefer asking the user to run it and paste back
real output over exhaustively re-deriving behavior from bytecode.

## Memory

Session-persistent project memory for this repo lives outside the working tree (Claude's own memory
system) — check it for prior investigation before re-deriving things like client API quirks,
known-flaky tests, or in-progress uncommitted work state.
2 changes: 1 addition & 1 deletion bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
<dependency>
<groupId>org.opensearch.client</groupId>
<artifactId>opensearch-java</artifactId>
<version>${opensearch.version}</version>
<version>${opensearch.rest.client.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
Expand Down
38 changes: 12 additions & 26 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ param(
[switch]$Debug,
[int]$DebugPort = 5005,
[switch]$DebugSuspend,
[switch]$NoMavenCache,
[switch]$PurgeMavenCache,
[string]$KarafHome,
[switch]$UseOpenSearch,
[string]$Distribution,
Expand All @@ -73,6 +71,7 @@ param(
[switch]$SkipMigrationTests,
[switch]$ResolverDebug,
[switch]$KeepContainer,
[switch]$SearchEngineLogs,
[switch]$NoMemorySampler,
[int]$MemoryInterval = 30,
[switch]$Javadoc,
Expand All @@ -88,22 +87,16 @@ $PSDefaultParameterValues['*:ErrorAction'] = 'Stop'
$script:BuildScriptInvocation = @($MyInvocation.Line)

# Defaults aligned with build.sh
$script:UseMavenCache = -not $NoMavenCache
$script:ItMemorySampler = -not $NoMemorySampler
$script:KarafDebugSuspend = if ($DebugSuspend) { 'y' } else { 'n' }

if ($Ci) {
$NoKaraf = $true
$script:UseMavenCache = $false
$env:BUILD_NON_INTERACTIVE = 'true'
$MavenQuiet = $true
$Javadoc = $true
}

if ($PurgeMavenCache) {
$script:UseMavenCache = $false
}

if ($UseOpenSearch -and [string]::IsNullOrWhiteSpace($Distribution)) {
$Distribution = 'unomi-distribution-opensearch'
}
Expand Down Expand Up @@ -270,8 +263,6 @@ function Show-Usage {
Write-Host ' -Debug Run Karaf in debug mode'
Write-Host ' -DebugPort PORT Set debug port (default: 5005)'
Write-Host ' -DebugSuspend Suspend JVM until debugger connects'
Write-Host ' -NoMavenCache Disable Maven build cache'
Write-Host ' -PurgeMavenCache Purge local Maven cache before building'
Write-Host ' -KarafHome PATH Set Karaf home directory for deployment'
Write-Host ' -UseOpenSearch Use OpenSearch instead of ElasticSearch'
Write-Host ' -Distribution DIST Set Unomi distribution (e.g. unomi-distribution-opensearch)'
Expand All @@ -286,10 +277,11 @@ function Show-Usage {
Write-Host ' -SkipMigrationTests Skip migration-related tests'
Write-Host ' -ResolverDebug Enable Karaf Resolver debug logging for integration tests'
Write-Host ' -KeepContainer Keep search engine container running after tests'
Write-Host ' -SearchEngineLogs Stream search engine Docker logs to the Maven console during integration tests'
Write-Host ' -NoMemorySampler Disable JVM/system memory sampling during integration tests'
Write-Host ' -MemoryInterval SEC Memory sample interval in seconds (default: 30)'
Write-Host ' -Javadoc Build and validate Javadoc after install'
Write-Host ' -Ci CI mode: no Karaf, no Maven cache, non-interactive, Javadoc'
Write-Host ' -Ci CI mode: no Karaf, non-interactive, Javadoc'
Write-Host ' -LogFile PATH Tee all output to PATH (console + file)'
Write-Host ' -LogFileOnly With -LogFile: write to file only, suppress console'
Write-Host ''
Expand Down Expand Up @@ -332,15 +324,6 @@ function Initialize-ProjectVersion {

Initialize-ProjectVersion

if ($PurgeMavenCache) {
Write-Host 'Purging Maven cache...'
$m2 = Join-Path (Get-UserHome) '.m2'
foreach ($dir in @('build-cache', 'dependency-cache', 'dependency-cache_v2')) {
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $m2 $dir)
}
Write-Host 'Maven cache purged.'
}

function Test-JavaRequirement {
if (-not (Test-CommandExists 'java')) {
Write-Status 'error' 'java not found'
Expand Down Expand Up @@ -535,6 +518,10 @@ function Test-Requirements {
Write-Status 'error' 'ItDebug enabled but integration tests are not enabled. Use -IntegrationTests.'
$hasErrors = $true
}
if ($SearchEngineLogs -and -not $IntegrationTests) {
Write-Status 'error' 'SearchEngineLogs enabled but integration tests are not enabled. Use -IntegrationTests.'
$hasErrors = $true
}
if ($ItDebug) {
if ($ItDebugPort -lt 1024 -or $ItDebugPort -gt 65535) {
Write-Status 'error' "Integration test debug port: $ItDebugPort (invalid)"
Expand All @@ -547,11 +534,6 @@ function Test-Requirements {
}
}

if ($Offline -and $PurgeMavenCache) {
Write-Status 'error' 'Cannot use -PurgeMavenCache in offline mode'
$hasErrors = $true
}

Write-Host ''
if ($hasErrors) {
Write-Status 'error' 'Critical requirements not met. Please fix the errors above.'
Expand All @@ -571,7 +553,6 @@ function Get-MavenArgumentList {
$args = @()
if ($MavenDebug) { $args += '-X' }
if ($Offline) { $args += '-o' }
if (-not $script:UseMavenCache) { $args += '-Dmaven.build.cache.enabled=false' }
if ($MavenQuiet) { $args += '-ntp' }
if ($env:MAVEN_EXTRA_OPTS) {
$args += $env:MAVEN_EXTRA_OPTS.Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries)
Expand Down Expand Up @@ -608,6 +589,10 @@ function Get-MavenArgumentList {
}
if ($SkipMigrationTests) { $args += '-Dit.test.exclude.pattern=**/migration/**/*IT.java' }
if ($KeepContainer) { $args += '-Dit.keepContainer=true' }
if ($SearchEngineLogs) {
$args += '-Ddocker.showLogs=true'
Write-Host 'Search engine Docker logs will stream to the Maven console (also written under itests/target/*0/logs/)'
}
if ($ResolverDebug) { $args += '-Dit.unomi.resolver.debug=true' }
if ($SkipUnitTests) { $args += '-Pskip-unit-tests' }
} else {
Expand Down Expand Up @@ -669,6 +654,7 @@ function Write-ItRunTraceStart {
"it.debug.suspend=$ItDebugSuspend"
"skip.migration.tests=$SkipMigrationTests"
"it.keep.container=$KeepContainer"
"it.search.engine.logs=$SearchEngineLogs"
"it.memory.sampler=$($script:ItMemorySampler)"
"it.memory.interval=$MemoryInterval"
"maven.debug=$MavenDebug"
Expand Down
Loading
Loading