Farooq Ayoade created FINERACT-2668:
---------------------------------------

             Summary: Bulk-import download templates load the entire tenant 
(every client and every account) into the .xls — O(tenant) memory and file size 
for an O(rows-entered) operation; can OOM the server
                 Key: FINERACT-2668
                 URL: https://issues.apache.org/jira/browse/FINERACT-2668
             Project: Apache Fineract
          Issue Type: Improvement
          Components: DataImportTool
            Reporter: Farooq Ayoade


h3. Observed behavior

Requesting a bulk-import *download template* for a transaction-type entity 
(e.g. loan repayment) builds an Excel workbook that embeds the {*}entire 
tenant{*}:
 * a hidden *{{Clients}}* sheet containing *every client* in the tenant (one 
row each),
 * a hidden *{{Offices}}* sheet with every office,
 * the data sheet itself carries lookup columns holding *every active account* 
in the tenant, plus a {{VLOOKUP}} array formula pre-written into a *hard-coded 
3000 rows × 5 columns* block.

The underlying fetches are {*}unbounded{*}. For loan repayment 
({{{}BulkImportWorkbookPopulatorServiceImpl.populateLoanRepaymentWorkbook{}}}):

{{List<ClientData>      clients = fetchClients(officeId);        // officeId 
null ⇒ retrieveAll(null), no LIMIT
List<LoanAccountData> loans   = fetchLoanAccounts(officeId);   // officeId null 
⇒ retrieveAll(null), no LIMIT}}

{{ClientReadPlatformServiceImpl.retrieveAll(SearchParameters)}} only appends a 
{{LIMIT}} clause when search parameters request one; called with {{null}} it 
returns {*}all rows{*}. The result set is then materialised as POI {{HSSFCell}} 
objects in heap.

On a tenant with, say, 100k clients an HQ-scoped user downloading a "simple 
repayment" template pulls 100k client rows + every active loan into memory and 
into a multi-megabyte {{{}.xls{}}}. Multiple concurrent downloads multiply the 
heap cost and can OOM the instance.
h3. Why this is wasteful — the importer never reads the loaded data

The {{Clients}} and {{Offices}} sheets exist *only* to drive Excel's cascading 
data-validation dropdowns (Office → Client → Account) during manual entry. The 
{*}import handler does not read them{*}. 
{{LoanRepaymentImportHandler.readLoanRepayment(...)}} resolves the loan purely 
from the typed account number:

{{String loanaccountInfo = 
ImportHandlerUtils.readAsString(LoanRepaymentConstants.LOAN_ACCOUNT_NO_COL, 
row);
loanAccountId = 
loanReadPlatformService.retrieveLoanIdByAccountNumber(loanAccountAr.get(0));}}

So the server already does an authoritative server-side lookup on upload. The 
entire embedded client/office dataset is a data-entry convenience that costs 
O(tenant) to build and is discarded at import time. (Only the small {{Extras}} 
sheet — payment types — is read back, via {{{}getIdByName{}}}.)
h3. Expected behavior
 * A download template must not be O(tenant size). Lookup data must be 
*bounded* and/or *scoped* (by office hierarchy), and {*}omittable{*}.
 * When a tenant exceeds a configurable lookup threshold, the template should 
*gracefully degrade* to a lean form (plain typed columns, no embedded 
client/office dataset, no 3000-row VLOOKUP block), relying on the server-side 
validation that already runs on upload.
 * Existing callers that depend on the dropdown UX keep working (backward 
compatible by default).

h3. Steps to reproduce
 # On a tenant with a large client/loan base (or seed several thousand 
clients), authenticate as a user whose office is the head office.
 # {{GET /fineract-provider/api/v1/loans/repayments/downloadtemplate}} (no 
{{{}officeId{}}}).
 # Observe: response is a multi-MB {{{}.xls{}}}; the {{Clients}} sheet contains 
every client in the tenant; opening the file in Excel is slow (15,000 pre-baked 
VLOOKUP formulas recalc). Under concurrent requests, watch server heap.

h3. Root cause
 # *Unbounded fetch.* {{fetchClients(null)}} / {{fetchLoanAccounts(null)}} call 
{{{}retrieveAll(null){}}}, which applies no {{{}LIMIT{}}}.
 # *Lookup sheets are mandatory and tenant-wide.* {{ClientSheetPopulator}} / 
{{OfficeSheetPopulator}} are always built and always span the whole 
(office-hierarchy-visible) tenant, even though the importer ignores them.
 # *Hard-coded 3000-row formula block.* Each transaction populator's 
{{setDefaults(...)}} writes {{VLOOKUP}} formulas into rows 1..3000 regardless 
of how many rows the user will actually enter.

This pattern is shared across the transaction-type templates (loan repayment, 
guarantor, savings/recurring/ fixed-deposit transactions, shared accounts, …), 
so the fix belongs in the shared populator path, not in any single handler.
h3. Proposed fix

Backward-compatible, opt-out-of-bloat design (default behaviour unchanged for 
small tenants):
 # *Bound every lookup fetch.* Introduce a configurable cap (e.g. 
{{{}fineract.bulkimport.template.max-lookup-rows{}}}, default ~10,000). 
{{fetchClients}} / {{fetchLoanAccounts}} request at most that many rows; honour 
{{officeId}} scoping that already exists on the endpoint.
 # *Make lookup sheets optional.* Add {{?includeLookups=\{true|false}}} to the 
download endpoints (default {{{}true{}}}). When {{{}false{}}}, skip 
{{ClientSheetPopulator}} / {{OfficeSheetPopulator}} and the in-sheet VLOOKUP 
cascade; emit a lean sheet of plain typed columns ({{{}Loan Account No.*{}}}, 
{{{}Amount Repaid*{}}}, {{{}Date*{}}}, {{{}Type*{}}}, payment-detail). Upload 
already validates the account number server-side.
 # *Auto-degrade.* If the bounded count would exceed the cap, automatically 
fall back to the lean template and write a note cell explaining that dropdowns 
were omitted and the account number must be typed.
 # *Stop hard-coding 3000 rows.* Replace the fixed 1..3000 VLOOKUP block with a 
small configurable default (or column/table references), and omit it entirely 
in lean mode.

Pilot the change on the *loan-repayment* template (smallest, highest-traffic 
reproducer); the shared-path changes then extend to the other transaction 
templates in follow-ups.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to