Summary
Running the Transaction Report (/json/getTransactionReport) exhausted the Java heap in production. After that the DB connection pool handed out dead connections, so even /auth/login failed until the server was restarted.
The evidence so far points to the report loading far more data than it needs, not to a slow memory or connection leak (see Timeline).
Reproduction
- Location: a warehouse with about 25k transactions.
- Go to Reports > Transaction Report.
- Set Start date
01/01/2026 and End date 08/31/2026, then click Submit.
- Result: the table shows no results (see "Date filter" below).
- Click Download Data (the CSV export).
- Result: the server runs out of heap and becomes unresponsive.
Timeline (2026-09-17, UTC)
| Time |
Event |
| 17:17 |
Server reported down |
| 17:38 |
First Sentry events (heap space, JDBC rollback failures) |
| ~17:42 |
Server restarted |
| 17:46 |
Java heap space again, 4 minutes after the restart |
| 18:03, 18:18, 19:15 |
More OutOfMemoryErrors |
| 19:25 to 09-18 01:48 |
DB timeouts / rollback failures continue |
A freshly restarted JVM (about 1.8 GB max heap) ran out of memory within minutes. So this report alone can exhaust the heap; an accumulated leak isn't needed to explain it.
Earlier observations (not conclusive on their own):
- Heap was about 90% used (1612 MB of 1820 MB) at a quiet moment. That was a snapshot of used heap, not what remains after GC, so it doesn't prove a leak.
- The VM uses only about 20% of its RAM, so the heap is capped well below what's available.
- The previous restart was on Aug 11.
Sentry
Root causes (code)
- Opening/closing balances load the full transaction history.
ReportService.getTransactionReport() calls productAvailabilityService.getAvailableItemsAtDateAsMap() for startDate and endDate. For past dates that calls InventoryService.getTransactionEntriesBeforeDate(), which loads every TransactionEntry before the date for every product with activity, as full Hibernate entities plus lazy loads. With an end date of 08/31/2026, that is essentially the location's entire history, loaded twice. (The 25k figure counts transactions; the entry count is several times higher.)
- Period entries are loaded as entities.
getFilteredTransactionEntries() returns full TransactionEntry objects that are then grouped and summed in Groovy. The report doesn't use transaction_fact.
- CSV "backdated" calculation (Download Data).
value.transaction.unique()...transactionEntries.flatten() loads every entry of every matching transaction, across all products, before filtering. This is the heaviest path.
- Concurrent retries. The AJAX timeout is 120s (
openboxes.ajaxRequest.timeout). The browser gives up, but the server thread keeps running, so each resubmit adds another full request.
- Connection held for the whole request.
JsonController has @Transactional on the class, so each request holds a connection and a growing Hibernate session until the JSON has been rendered.
- Pool hands out dead connections.
testOnBorrow: false in application.yml. Also, maxAge: 10 * 60000 is a plain YAML string, not a number, so it may not be taking effect.
Date filter "not working"
This is most likely a silent timeout, not a date-binding bug. An 8-month range runs longer than the 120s AJAX timeout. jQuery then reports xhr.status == 0, and the fnServerData error handler in showTransactionReport.gsp returns without showing anything. The user sees an empty table while the server keeps working. Date binding looks correct (MM/dd/yyyy is in grails.databinding.dateFormats). Confirm after the performance fix.
Proposed fix
Code
Config / server
Summary
Running the Transaction Report (
/json/getTransactionReport) exhausted the Java heap in production. After that the DB connection pool handed out dead connections, so even/auth/loginfailed until the server was restarted.The evidence so far points to the report loading far more data than it needs, not to a slow memory or connection leak (see Timeline).
Reproduction
01/01/2026and End date08/31/2026, then click Submit.Timeline (2026-09-17, UTC)
A freshly restarted JVM (about 1.8 GB max heap) ran out of memory within minutes. So this report alone can exhaust the heap; an accumulated leak isn't needed to explain it.
Earlier observations (not conclusive on their own):
Sentry
Root causes (code)
ReportService.getTransactionReport()callsproductAvailabilityService.getAvailableItemsAtDateAsMap()forstartDateandendDate. For past dates that callsInventoryService.getTransactionEntriesBeforeDate(), which loads everyTransactionEntrybefore the date for every product with activity, as full Hibernate entities plus lazy loads. With an end date of 08/31/2026, that is essentially the location's entire history, loaded twice. (The 25k figure counts transactions; the entry count is several times higher.)getFilteredTransactionEntries()returns fullTransactionEntryobjects that are then grouped and summed in Groovy. The report doesn't usetransaction_fact.value.transaction.unique()...transactionEntries.flatten()loads every entry of every matching transaction, across all products, before filtering. This is the heaviest path.openboxes.ajaxRequest.timeout). The browser gives up, but the server thread keeps running, so each resubmit adds another full request.JsonControllerhas@Transactionalon the class, so each request holds a connection and a growing Hibernate session until the JSON has been rendered.testOnBorrow: falseinapplication.yml. Also,maxAge: 10 * 60000is a plain YAML string, not a number, so it may not be taking effect.Date filter "not working"
This is most likely a silent timeout, not a date-binding bug. An 8-month range runs longer than the 120s AJAX timeout. jQuery then reports
xhr.status == 0, and thefnServerDataerror handler inshowTransactionReport.gspreturns without showing anything. The user sees an empty table while the server keeps working. Date binding looks correct (MM/dd/yyyy is ingrails.databinding.dateFormats). Confirm after the performance fix.Proposed fix
Code
SUM(quantity) ... GROUP BY inventory_item, bin_location), or usetransaction_fact, instead of loading entities.getTransactionReportread-only (@Transactional(readOnly = true)) and stop holding the connection during rendering.Config / server
testOnBorrow: trueandmaxAge: 600000.-XX:+HeapDumpOnOutOfMemoryErrorto confirm or rule out a leak on the next occurrence.-Xmx/-Xmsagainst the VM's RAM (short-term relief).