Skip to content

CAMEL-23691: Improve CaseInsensitiveMap with O(1) hash table and header key deduplication#23766

Merged
gnodet merged 2 commits into
apache:mainfrom
gnodet:spangled-spectrum
Jun 4, 2026
Merged

CAMEL-23691: Improve CaseInsensitiveMap with O(1) hash table and header key deduplication#23766
gnodet merged 2 commits into
apache:mainfrom
gnodet:spangled-spectrum

Conversation

@gnodet
Copy link
Copy Markdown
Contributor

@gnodet gnodet commented Jun 4, 2026

CAMEL-23691

Summary

Replace the TreeMap-based CaseInsensitiveMap with a custom hash table implementation, improving the performance of every header access in every Camel route.

CaseInsensitiveMap is the default headers map for every Exchange in Camel. It is the most accessed data structure in the framework — every message.getHeader(), setHeader(), removeHeader(), and header iteration goes through it.

Before (TreeMap with CASE_INSENSITIVE_ORDER)

  • O(log n) for get, put, containsKey, remove
  • String comparisons at every tree node during lookup
  • Deserialized header keys allocate duplicate String objects across exchanges
  • To get O(1), users needed the camel-headersmap dependency (cedarsoftware java-util, which uses ThreadLocal internally — problematic with virtual threads)

After (custom hash table)

  • O(1) for get, put, containsKey, remove
  • Zero allocation on lookups — case-insensitive hash computed char-by-char with Character.toLowerCase(Character.toUpperCase(c)), keys compared with equalsIgnoreCase(). No toLowerCase() string copies
  • Header key deduplication — well-known Exchange header constants (151 keys) are registered at startup via ExchangeConstantProvider; deserialized keys matching a known constant are replaced with the canonical interned reference, reducing memory across exchanges
  • Entries stored in insertion order (deterministic iteration)
  • Original key case preserved (first-put case wins, same as before)
  • No external dependencies, no ThreadLocal, no reflection
  • Eliminates the need for camel-headersmap in most cases

Design

The map extends AbstractMap and implements its own hash table with separate chaining using parallel arrays (keys[], values[], chainNext[]). This gives cache-friendly storage and avoids per-entry object allocation overhead of HashMap.Node.

The hash function uses the same two-step fold as String.equalsIgnoreCase()Character.toLowerCase(Character.toUpperCase(c)) — ensuring hash consistency even for Unicode edge cases (e.g., Turkish dotless-ı).

DefaultHeadersMapFactory registers all Exchange string constants at startup via ExchangeConstantProvider.values()CaseInsensitiveMap.registerKnownKeys(). On put(), a zero-allocation lookup in the static known-keys table replaces deserialized keys with canonical references.

Compatibility

  • The Map<String, Object> API is unchanged
  • DefaultHeadersMapFactory and instanceof CaseInsensitiveMap checks work as before
  • No code in the codebase uses SortedMap/NavigableMap methods on this map
  • Iteration order changes from alphabetical (TreeMap) to insertion order — no code depends on alphabetical ordering
  • All existing tests pass; 10 new tests added

Test plan

  • CaseInsensitiveMapTest — 32 tests (+ 10 new: insertion order, iterator removal, entry setValue, entrySet remove, remove-then-reput, resize/rehash, containsValue, null values, clear-reuse, known key deduplication)
  • DefaultMessageHeaderTest — 16 tests
  • Formatter and import sort clean

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions github-actions Bot added the core label Jun 4, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

🧪 CI tested the following changed modules:

  • core/camel-api
  • core/camel-base-engine
  • core/camel-core
  • core/camel-util
  • tooling/maven/camel-package-maven-plugin

ℹ️ Dependent modules were not tested because the total number of affected modules exceeded the threshold (50). Use the test-dependents label to force testing all dependents.

⚠️ Some tests are disabled on GitHub Actions (@DisabledIfSystemProperty(named = "ci.env.name")) and require manual verification:

  • core/camel-core: 2 test(s) disabled on GitHub Actions
Build reactor — dependencies compiled but only changed modules were tested (5 modules)
  • Camel :: API
  • Camel :: Base Engine
  • Camel :: Core
  • Camel :: Maven Plugins :: Camel Maven Package
  • Camel :: Util

⚙️ View full build and test results

…er key deduplication

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gnodet gnodet force-pushed the spangled-spectrum branch from f1b4400 to a941208 Compare June 4, 2026 14:21
@gnodet
Copy link
Copy Markdown
Contributor Author

gnodet commented Jun 4, 2026

Claude Code on behalf of Guillaume Nodet

Addressed review feedback: replaced reflection with ExchangeConstantProvider.values(). Added a values() method to the generated constant provider class (via the Velocity template in camel-package-maven-plugin) so DefaultHeadersMapFactory can register known keys without reflection — Quarkus-friendly.

Copy link
Copy Markdown
Contributor

@davsclaus davsclaus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid replacement for the TreeMap-based CaseInsensitiveMap — O(1) lookups, zero-allocation hash, and key deduplication via ExchangeConstantProvider (no reflection). The hash function correctly uses the two-step Unicode fold matching String.equalsIgnoreCase(). 10 new tests cover insertion order, iterator removal, resize, null values, and key dedup.

Minor observations (non-blocking):

  • Import ordering in constant-provider.vm: java.util.Collection is placed after javax.annotation.processing.Generated. The build formatter fixes the generated file, but the template itself has the wrong order.
  • MapEntry.equals() uses case-sensitive key comparison (per Map.Entry contract), while the map is case-insensitive. Only matters for direct entry comparison — EntrySet.contains()/remove() correctly use case-insensitive findIndex().
  • After many put/remove cycles without resize, tombstoned slots accumulate in the entry arrays. Fine for typical short-lived Exchange headers, but worth noting for long-lived maps with high churn.

This review does not replace specialized AI review tools (CodeRabbit, Sourcery) or static analysis (SonarCloud).

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@davsclaus
Copy link
Copy Markdown
Contributor

We can deprecate camel-headersmap

@davsclaus
Copy link
Copy Markdown
Contributor

@gnodet gnodet changed the title Improve CaseInsensitiveMap: O(1) hash table with zero-alloc lookups and header key deduplication CAMEL-23691: Improve CaseInsensitiveMap with O(1) hash table and header key deduplication Jun 4, 2026
@gnodet gnodet merged commit da023d6 into apache:main Jun 4, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants