#886: AbstractRechnungCache.kt

projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AbstractRechnungCache.kt

Type: Kotlin abstract class extending AbstractCache — Cache layer for invoice calculations

Purpose: Caches pre-calculated invoice totals (RechnungInfo) and position calculations (RechnungPosInfo) to avoid redundant recalculation. Supports both outgoing invoices (RechnungDO) and incoming invoices (EingangsrechnungDO) through its generic type parameter.

Source path: projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AbstractRechnungCache.kt

136 lines · 79 code · 44 comments · 13 blank

An in-memory performance cache layer on top of the AbstractCache framework. Invoice calculations (net/gross totals, VAT breakdown, cost assignments) are expensive because they require traversing the entire position tree with cost allocation lookups. This cache stores pre-computed RechnungInfo and RechnungPosInfo objects keyed by entity ID, with automatic refresh via the CacheHelper scheduling mechanism. The cache was refactored from synchronized blocks to ConcurrentHashMap to eliminate a deadlock.

Type Parameters & Constructor

abstract class AbstractRechnungCache(
    val entityClass: KClass<out AbstractRechnungDO>,
    protected val rechnungJdbcService: RechnungJdbcService,
) : AbstractCache()
ParameterDescription
entityClassThe Kotlin class reference for the invoice entity type (RechnungDO::class or EingangsrechnungDO::class) — used for JDBC queries and logging
rechnungJdbcServiceJDBC service for efficient bulk-loading of invoice info from the database (bypasses Hibernate for performance)

Internal State

FieldTypeDescription
invoiceInfoMapConcurrentHashMap<Long, RechnungInfo>Primary cache: invoice ID → aggregated invoice calculations
invoicePosInfoMapConcurrentHashMap<Long, RechnungPosInfo>Secondary cache: position ID → position-level calculations

Both maps use ConcurrentHashMap (not synchronized blocks) — this change was made in commit 85d8930b3 to fix a deadlock that occurred when cache operations interacted with each other under synchronization.

Public API

update(invoice: AbstractRechnungDO)

Forces recalculation of a single invoice and stores the result in the cache. Called when an invoice is modified (inserted, updated, or deleted through the DAO layer). Uses RechnungCalculator.calculate(invoice) to compute fresh values.

ensureRechnungInfo(rechnung: AbstractRechnungDO): RechnungInfo

Guarantees a RechnungInfo exists for the given invoice. If already cached, returns the cached version (and sets rechnung.info). Otherwise calculates it via RechnungCalculator, stores in the cache, and returns. This is the preferred accessor — it works even if the position tree hasn't been lazy-loaded yet.

getRechnungInfo(rechnungId: Long?): RechnungInfo?

Returns cached invoice info by ID (null-safe for the ID parameter). Calls checkRefresh() which ensures the cache is not stale.

getRechnungInfo(rechnung: AbstractRechnungDO?): RechnungInfo?

Overload that extracts the ID from the entity. Null-safe.

ensureRechnungPosInfo(pos: AbstractRechnungsPositionDO): RechnungPosInfo

Analogous to ensureRechnungInfo but for individual positions. Ensures the parent invoice's RechnungInfo exists first (needed as context), then calculates position-specific info via RechnungCalculator.calculate(posInfo, pos).

getRechnungPosInfo(rechnungPosId: Long?): RechnungPosInfo?

Returns cached position info by ID. Null-safe.

Cache Refresh (override fun refresh())

Called automatically by the CacheHelper framework on a timer and during the checkRefresh() guard. The refresh strategy uses copy-and-swap for thread safety:

  1. Creates new ConcurrentHashMap instances (nInvoiceInfoMap, nInvoicePosInfoMap)
  2. Bulk-loads all invoice info via rechnungJdbcService.selectRechnungInfos(entityClass) — this uses JDBC directly for performance, bypassing Hibernate
  3. Populates both maps from the JDBC result set
  4. Atomically swaps the new maps in place of the old ones

This approach means the cache is never stale for longer than one refresh cycle, and reads never block on the refresh operation.

Architecture Notes

Git History

868d6abb7 2025 -> 2026
85d8930b3 AbstractRechnungCache: Deadlock removed by replacing sync blocks through ConcurrentHashMaps
63081666f Source file headers: 2024-> 2025.
2d324f2bb Finance: incoming invoices fixed.
12610fee2 Migration stuff in progress... (all tests of all packages: OK).
ff2cc4cfa Migration stuff in progress... (all tests of all packages: OK).
190c0aea1 Migration stuff in progress... (all tests of all packages: OK).
2e9839891 Migration stuff in progress...
ba2479571 Migration stuff in progress...
b3293f0cc PersistenceService/Context: stats handling improved.
b47c21af6 Refactored caching and calculations with invoices (not yet finished)
ccb7ca64d Migration stuff in progress...
41b5c2645 Migration stuff in progress...
87dd5b87c AuftragsCache refactored, migration stuff... (all tests OK)
9408b59d7 Migration stuff in progress...
85b4e1175 PfPersistenceService and PfPersistenceContext: query renamed to executeQuery.
4c04cfd65 MAJOR-CHANGE! Migration of integer id's to Long id's (including fk's etc.)
2863b30b9 Migration stuff in progress...
067a4cbb1 Migration stuff in progress...
9d8b94352 Migration stuff in progress...
67ce75fe9 Migration stuff in progress...
e33c8b9c2 Migration stuff in progress...