Bug #11015
allow cleanup the DMO types, caches, etc for dynamic temp-tables
100%
History
#2 Updated by Constantin Asofiei 7 months ago
- Status changed from New to WIP
- Assignee set to Constantin Asofiei
- Priority changed from Normal to High
A customer uses heavily XMLs to communicate with the FWD server, which in turn creates dynamic temp-tables, and in most cases, the name of the temp-table from XML schema is almost random. This will end up with an always increasing JVM metaspace as all DMO types live forever at this time, plus some leaks in i.e. SchemaDictionary.cachedScopes and other caches, like DmoMeta, etc
This test shows how to stress-test FWD:
def temp-table tt1 field f1 as int.
def var hq as handle.
def var htt as handle.
def var i as int.
def var n as int.
n = 10000.
do i = 1 to n:
create temp-table htt.
htt:create-like("tt1").
htt:add-new-field("v" + string(i), "integer").
htt:temp-table-prepare("tx" + string(i)).
create query hq.
hq:add-buffer(htt:default-buffer-handle).
hq:query-prepare("for each tx" + string(i)).
hq:query-open().
hq:get-first().
hq:query-close().
delete object hq.
delete object htt.
end.
What I'm trying now is to:
- mark the DmoMeta as 'deleted' when a dynamic temp-table gets deleted.
- limit the number of cached
DmoMetainstances, and when they are evicted, unload also from theAsmClassLoader - check which caches also need to be maintained (like in
SchemaDictionary) - there is
SchemaCheckfor dynamic perm tables, I'll see how I can use parts of that code.
#4 Updated by Alexandru Lungu 7 months ago
limit the number of cached DmoMeta instances, and when they are evicted, unload also from the AsmClassLoader
This will require a cache with lenient eviction policy.
#5 Updated by Constantin Asofiei 7 months ago
BufferManager.buffersByDMOBufClassregisters temp-tables with null multiplex ID - this will not allow them to be processed or removed properly. I've changed this to use the DMO and the lookup to check for the multiplex ID matchDmoMetadataManager.byId- this needs to be switched to a map and maintained properly. Alexandru: if you have some other customer heaps for long running FWD servers, please check this array's length in Eclipse MATUniqueTracker- optimization for the no-op instance for temp-tables - there is no need for usingdbMapSchemaDictionary- leak intableIndexes, this should not be used for runtime conversion
TBD: for each dynamic temp-table, double-check the SQL H2 table name, if one is being created distinctly and it gets dropped properly.
TBD: to clean PropertyHelper's g2pCache, p2pgCache, p2psCache caches, SortIndex.cache and IndexHelper.cache. Some of them may be set to a LRUCache and with a size limit.
TBD: make the DMO registry a LRU cache and on evict clean up the artifacts/caches/etc
#6 Updated by Ovidiu Maxiniuc 7 months ago
I think these is good news.
Constantin Asofiei wrote:
DmoMetadataManager.byId- this needs to be switched to a map and maintained properly. Alexandru: if you have some other customer heaps for long running FWD servers, please check this array's length in Eclipse MAT
This is the array I was talking about in our meet today. Switching to a Map may be costly, and reusing the table IDs is not ideal.
However, there is another good news: I took a quick look at the usages of this, and the getDmoInfo() method (the only read from the data structure) is not used any longer. I think we can completely drop it now.
Note: in DmoMeta, there is a parallel id property, but it is (now) unrelated to the our table. It has its own idSeed generator and is actively used in many places. Even if these IDs may coincide, the byId is never used to search for the DMO.
Those being said, Alexandru, I think you can ignore Constantin's request.
#7 Updated by Alexandru Lungu 7 months ago
DmoMetadataManager.byId - this needs to be switched to a map and maintained properly. Alexandru: if you have some other customer heaps for long running FWD servers, please check this array's length in Eclipse MAT
In MAT, I see byId with 3070 slots. Skimming, some of them are persistent tables, some are temp-tables, some a are dynamic temporary tables. I spotted dt200 the highest.
#8 Updated by Alexandru Lungu 7 months ago
BTW: there are ~4376 temp DMOs and 958 persistent DMOs in the application profiled. So 3070 is not that much IMHO
#9 Updated by Constantin Asofiei 7 months ago
- % Done changed from 0 to 10
- Status changed from WIP to Review
- reviewer Alexandru Lungu added
Alexandru, please review 11015a - this contains a mix of fixes for persistence. I'll use 11015b for the DMO unload.
#10 Updated by Alexandru Lungu 7 months ago
Review for 11015a:
- Changes in BufferManager are dependent upon the unique pk across temp-tables; which I think was fixed in H2 in the mean-time, right? In other words, we can't afford to have these changes in BufterManager without the guarantee that the PK provided will find records only in the expected multiplex.
- I am OK with the changes in SchemaDictionary at the price of performance drop.
- However, cachedScopes was the main culprit in memory leaks when not clearing buffers. I expect this to accumulate as well. I think the key is dynamically computed.
- The point is that we can also disable cachedScopes for a quick fix, but if we opt to keep it and assess how DMOs impls can be unloaded, then the same solution can be applied to tableindexes.
- UniqueTracker changes aim performance; good catch getDmoInfo was removed, so byld is removed. This is good.
Sorry for taking so long; I prepared myself for reviewing the DMO unload as well. Now I spotted that they are not ready yet. I will provide "my preparation" for review. It may be quite long:
- Prerequisite: We have a candidate for unloading only when the dynamic temp-table is deleted. This implies that its buffers are deleted which in turn make the queries and datasets invalid. This should happen now in trunk; if not, this should be fixed first.
- The "hard" relations toward the DMO impl are mostly Class> for that class, BufferImpl/TemporaryBuffer for that DMO, DMOMeta and RecordMeta. There may be others, but I searched the project for classes that retain such information.
FieldReferenceretains DmoMeta and these can be referenced by converted code. These should be handled properly. I think they are quite intensively used internally, so this requires good testing.RecordBufferretains RecordMeta and DmoMeta. These should be marked as deleted. Buffers are not cached, so there is no evident leak here.- Queries may be retained by converted code (handles), so we need to ensure that these queries are closed completely, as components, sort criterions, etc. still hold the DMO. Internal queries should do the same (can't think of any yet). Queries are not cached AFAIK, so there are no evident leaks here.
SQLQueryobjects are cached androwStructurereference Class. I might think that the cache will eventually expire the queries with unloaded classes, but this should be tested.FQLPreprocessorreferences dmoIfaces and is cached.FQLHelperreferences meta and is cached.FQLToSQLConverterreferences aliases (DmoMeta) andCompositeDmoInfo, which in turn stores parentDmoInfo. AFAIK, this is not cached.
- Datasets: same story as queries.
- Relations and joins: I found
Relationannotation andRelatienJoethat retain Class. I am thinking of examples where datasets have both deleted and non deleted tables, so relations are broken. Joins are usually triggered by OF keyword and are retained by queries.DmoMeta.putRelationcan make a DMO meta reference another meta. The former one can be unloaded, so the DMOMeta of a still alive DMO may become corrupt.
- serialization:
JavaArgumentsParser,RecordSerializer.fromJsonandRecordBufferSerializereither use Class or retaindmoMeta. I am not very familiar with these, but I can imagine that unloading of DMO can affect these if they occur after deleting a DMO impl. - deserialization:
XmlExportandJsonExportcache objects that indirectly refer to the DMO impl:tableSchemasortables. Unless we ensure these are initialized, used and deleted before the DMO impl is unloaded, then we are safe. ChangeBrokermay register several listeners that provide buffers which in turn reference unloaded DMOs. I can't tell for sure if there is something obvious to be done here; testing is deemed (change a buffer in a query with multiple components, out of which one has an unloaded class).FastCopyHelperhas bundles which are cached, which in turn reference dmo meta and Class>
UnsafePersistenceintakes DMOMeta through direct-java access. We need to take care of customers that use unsafe persistence and/or public API with unloaded DMOs.- can't tell what
Record.fromPOJOis or whether it relates to unloading? - can't tell if buffer arguments, binding and by-reference can be affected by unloading.
- other classes
Record,PostCompilationOpsDriver,AssociationSyncher,ControlFlowOps,DataObjectFactory,TempTableResultSet,RecordMeta,TempTableHelper,TableMapper,LegacyTableInfo,DmoMetadataManagerwhich still require analysis. - OO classes may be affected by this unloading. I can imagine some standard 4GL OO classes that retain information about dynamic temp-tables (or use them), so some research should be done in this area.
- Class persisting (cold time optimization) is certainly affected by unloading; we need to decide which classes we need to persist to disk. Does unloading also evict from disk?
#11 Updated by Constantin Asofiei 6 months ago
Alexandru Lungu wrote:
Review for 11015a:
- Changes in BufferManager are dependent upon the unique pk across temp-tables; which I think was fixed in H2 in the mean-time, right? In other words, we can't afford to have these changes in BufterManager without the guarantee that the PK provided will find records only in the expected multiplex.
There is no key which uses the PK in the buffersByDMOBufClass dictionary - and any check inside the scoped sets is done by also validating the multiplex ID for temp-tables, beside the PK.
- However, cachedScopes was the main culprit in memory leaks when not clearing buffers. I expect this to accumulate as well. I think the key is dynamically computed.
This is still under investigation.
#12 Updated by Constantin Asofiei 6 months ago
- Status changed from Review to WIP
Branch 11015a was merged to trunk rev 16397 and archived.
#13 Updated by Constantin Asofiei 5 months ago
- Status changed from WIP to Review
- % Done changed from 10 to 90
- reviewer Eric Faulhaber, Ovidiu Maxiniuc added
Branch 11015b was created from trunk rev 16410.
In rev 16411, I've added the cache for the DMOMeta instances for dynamic temp-table - on eviction, it will initiate the clearing of the associated artifacts (DMO types, proxies, cache, etc), and this will allow the garbage collector to eventually clean up java.lang.Class instances from the meta space.
This cleanup is not done immediately: it is postponed on the next top-level block exit. Classes will remain in memory until the caches which use them will evict their references, and at that point they will be garbage-collected. Also, the H2 SQL table for a temp-table will be dropped the same way (on next top-level block exit).
So the approach is to allow the JVM to eventually be able to cleanup these proxies, DMOs and other caches, as the FWD application server continues running. If the dynamic temp-tables are minimal in use, then this cache will have no effect.
The default cache limit is 16k; I've checked ETF and that builds ~3k for the search tests suite. But, there is a small performance problem with this suite - I'm looking into it.
In the mean time, please review (keep in mind I still need to add history entries).
I also need to test the persistent jar with dynamic DMOs (see DynamicTablesHelper). Teodor - for #11100 you are already looking into how this pre-loading works. Please test also with 11015b and AppCds.
#15 Updated by Alexandru Lungu 5 months ago
AsmClassLoader- seems okCacheManager- is okDynamicConversionHelper- seems okDynamicTablesHelper- I spent quite a while in this class. I am mostly OK with the changes, but I need a bit more testing time with it.DmoMetadataManager- I think
Map<String, DmoMeta> sMap = simpleRegistry.get(dmoMeta.getSchema());should also be part of the synchronized black on delete inDmoMetadataManager.registerDmo; otherwise there can be a window of time in the delete process when registry doesn't have the DMO, butsimpleRegistryhas it. dynamicRegistryshould also be synchronized (?) from the same reasons assimpleRegistry? In other words,getDmoInfoshould also wrapsimpleRegistrywork with a synchronized block, right?- All registry actions are syncrhonized - makes sense.
- dynamicRegistry is not updated in the delete listener as this is called only on evict - makes sense.
- I think
DmoMeta- is ok- I would prefer a sanity check in
decrementUsageto avoid going from 0 to -1 when decrementing. this.usage == -1 || this.usage != 0this looks redundant. If it was made intentional, then it makes sense to stay like this for better clarity; if it is a leftover, then it can be adjusted.
- I would prefer a sanity check in
This cleanup is not done immediately: it is postponed on the next top-level block exit. Classes will remain in memory until the caches which use them will evict their references, and at that point they will be garbage-collected. Also, the H2 SQL table for a temp-table will be dropped the same way (on next top-level block exit).
The eviction within the same-session happens when the top-level is exit, but relative to another session, the eviction can happen in a critical path? If session A and B coexist and session A evicts the DMO at the end of top-level, then from B POV it will happen at an arbitrary point of time, right? I am not saying something is wrong per-se, but I want to understand why the postpone is relevant for same-session, but irrelevant for cross-session. In other words, executeOnReturn is something fragile as it means the return of the procedure in the session in which the expiration happened ... which is completely non-deterministic anyway.
I am planning to continue reviewing the other classes as well and do some testing.
#16 Updated by Ovidiu Maxiniuc 5 months ago
Review of 11015b, r16411
First, to note that all affected files lack the H entries and possible (c) year update. I'm sure they were skipped now to allow a smoother rebase process and the will be added at a later stage.
DynamicTablesHelper- line 1271: the cast to
TemporaryBufferis not necessary because thegetParentTable()method is defined inRecordBuffer. The test forrbuf.isTemporary()is also not required since the parent table isnullfor permanent tables, but it can be kept as an optimisation;
- line 1271: the cast to
DynamicTablesHelper- both
getHibernateFieldNamecan be dropped. To my knowledge they are (no longer) in use; createDynamicDMO: there are 3 calls todmoMeta.incrementUsage(), but only onedecrementUsage()(fromTTB.forceDelete()). There is no clear pairing between these so I think there are some chances that the balance is affected causing theusagenot to be properly decremented therefore making a DMO unable to be unloaded;deregisterDmoUser()method: I think the cleanup is a bit over-engineered. A simpleget()instead ofgetOrDefault()following by anullcheck would be a more efficient;evict()method: IMHO, we do not need to panic and re-throw in caseClassNotFoundException.dynamicRegistry: beside the 'natural' expiring of content, the entries are not removed on demand, when the DMOs are evicted. Shouldn't we do it instead of waiting the evicted DMOs to expire when the cache is full?
- both
DmoMeta- line 594: personally, I prefer
1Lbecause on some fonts (Courier)1llooks like11; - line 649:
usage -1is a particular case ofusage != 0. I think this should this should be rewritten asif (this.usage -1 || this.usage > 0)to better reflect the two situations:<static> || <dynamic-in-use>. As it appears on line 627.
- line 594: personally, I prefer
I have not found any issue in the other modified files. At any rate, the code should work. The above notes are not show-stoppers but I think that possibly there might still be some leaks, as described.
#17 Updated by Constantin Asofiei 3 months ago
- % Done changed from 90 to 100
In 11015b/16519 I've addressed these:
Review report:
- [CRITICAL] functional
ProxyFactory.CacheKey.hashCode: After convertingparentandinterfacestoWeakReference, the precomputed hash usesparent.hashCode()andArrays.hashCode(interfaces), which hash theWeakReferencewrapper identity (WeakReference does not overridehashCode). Butequalsdereferences via.get().equals(...). TwoCacheKeyinstances built from the same parent+interfaces produce different hashes and never collide in thecacheHashMapconsulted bygetProxyImpl, so the proxy class cache misses on every lookup, regenerating a fresh proxy class per invocation and leakingHashMapentries. Fix: derive the hash fromparent.get()and each dereferenced interfaceClass, treating cleared referents as an expired/tombstone key.
This is not a problem, the interfaces mentioned here is the local variable and not the instance field which is a WeakReference.
- [MAJOR] functional
ProxyFactory.CacheKey.equals:equals(andtoString) dereferenceWeakReference.get()without null-guarding. Once a referencedClassis GC'd, any later hash-collision probe oncache(a plainHashMapwith strongly-heldCacheKeyentries) walks the stale key and NPEs. NoReferenceQueue-driven eviction exists (TODO open in source), so stale keys accumulate unboundedly after the dynamic-type-unloading feature takes effect. Null-checkget()and wire aReferenceQueueto purge entries when parent/interface classes are collected.
Solved
[MAJOR] functional TemporaryBuffer.Context.executeDynamicTableCleaners: When !dropIterators.containsKey(dmoIface), the method returns immediately instead of using continue. All subsequent entries in dropTableListeners are silently skipped, dropTableListeners.clear() is never reached, and those entries accumulate and repeat on every future invocation.
Fixed
- [MAJOR] functional
AsmClassLoader.unloadClass:AsmClassLoader loader = delegates.remove(name); loader.loaded.remove(name);— ifnameis absent fromdelegates(double-unload, concurrent unload race, or class defined via a path that didn't register),loaderis null and the next line NPEs. Add a null check.
Fixed
- [MAJOR] functional
DynamicConversionHelper.prepareDynamicTables:astRootsstores only the firstAbstractTempTableencountered for a givendbName, but the cached schema scopekeyis derived from the combinedsbof all contributing buffers. The delete listener is registered only on that single storedtt, so if any other dynamic table contributing tokeyis deleted first, the listener never fires and the scope retains stale schema; conversely, deletion of the storedttevicts the scope prematurely while other tables remain live. Register the listener on every dynamicttcontributing tokey, or include per-tt identity in the key.
Fixed
- [MAJOR] performance
DmoMetadataManager.getDmoInfo/getDmoMeta:registryis already aConcurrentHashMap, yet the newsynchronized (DmoMetadataManager.class)wrappers serialize simplegetcalls on the FQL/query hot path against the long-running class-synchronizedregisterDmo(assembly + ASM class loading). Drop the synchronization on read paths.
Works as designed.
- [MAJOR] performance
DmoMeta.evict:evict()invokes delete listeners while holding the DmoMeta monitor. Listeners call into other synchronized structures (cache lock, class loaders,DmoMetadataManagermaps); the sibling cache-eviction code inDmoMetadataManager.initializeCachealready defers viaexecuteOnReturnspecifically to dodge this lock-order class. Snapshot the listener list under the monitor, release, then invoke.
Works as designed.
- [MINOR] functional
ProxyFactory.removeProxy: Only removes theclassCtorsentry but does not callAsmClassLoader.unloadClass, which is the mechanism that actually removes the delegate loader pinning the generated class. Without that call the class remains pinned byAsmClassLoader.delegates.removeProxyalso leaves thecachemap'sCacheKeyentry in place (values are weak, so no pinning, but stale entries accumulate). Coordinate both removals in one entry point.
Not its responsibility.
- [MINOR] functional
HandleResource.deregisterResource: Uses rawClassparameter (and matchingregisterResource), inconsistent with the declaredMap<Class>, String>. Change toClass>.
Fixed
- [MINOR] functional
ProcedureManager.executeOnReturn(new static): Delegates towork.get().executeOnReturn(...); if invoked outside a procedure scope, the inner method logs a WARNING and silently drops the runnable. For lifecycle-critical dynamic temp-table / proxy teardown, a silent drop leaks the dynamic class indefinitely. Document the active-scope precondition in javadoc and consider a session-scope fallback for lifecycle-critical callers.
Works as designed.
- [MINOR] performance
TemporaryBuffer.registerDynamicTableCleaner: Registers a distinctexecuteOnReturn(ctxt::executeDynamicTableCleaners, WeightFactor.LAST)handler for each dynamic temp-table; with N dynamic tables in a scope, N identical handlers are registered. The first invocation drainsdropTableListenersand subsequent ones short-circuit viaisEmpty(), so runtime cost is bounded, but registration/memory overhead is linear in dynamic-table count. Use a per-context "already registered" flag.
Fixed
- [MINOR] performance
DynamicConversionHelper.prepareDynamicTables:tt.getDmoMeta().addDeleteListener(...)is called whenever a scope is freshly cached;addDeleteListenerperforms no dedup. Scope caching and DmoMeta lifetimes are decoupled, so a given DmoMeta can accumulate duplicate listeners across re-cache cycles, each firingdict.removeScope(key)(idempotent but wasted work and listener-list bloat). Dedup inaddDeleteListeneror at the call site.
Fixed, but not in terms of 'dedup' - any DMO which gets evicted must remove this from the cache, as the entire dictionary gets invalidated.
From Ovidiu:DynamicTablesHelper- line 1271: the cast to
TemporaryBufferis not necessary because thegetParentTable()method is defined inRecordBuffer. The test forrbuf.isTemporary()is also not required since the parent table isnullfor permanent tables, but it can be kept as an optimisation;
No longer applies
- line 1271: the cast to
DynamicTablesHelper- both
getHibernateFieldNamecan be dropped. To my knowledge they are (no longer) in use;
Dropped.
- both
createDynamicDMO: there are 3 calls todmoMeta.incrementUsage(), but only onedecrementUsage()(fromTTB.forceDelete()). There is no clear pairing between these so I think there are some chances that the balance is affected causing theusagenot to be properly decremented therefore making a DMO unable to be unloaded;
Only one call can be executed for acreateDynamicDMOinvocation:
- if already cached
- if not cached but loaded from persistent cache
- you are now converting the DMO
deregisterDmoUser()method: I think the cleanup is a bit over-engineered. A simpleget()instead ofgetOrDefault()following by anullcheck would be a more efficient;
I like it like this :)
evict()method: IMHO, we do not need to panic and re-throw in caseClassNotFoundException.
Fixed
dynamicRegistry: beside the 'natural' expiring of content, the entries are not removed on demand, when the DMOs are evicted. Shouldn't we do it instead of waiting the evicted DMOs to expire when the cache is full?
This was the intended design: only when the cache can no longer keep it, we evict it.
DmoMeta- line 594: personally, I prefer
1Lbecause on some fonts (Courier)1llooks like11;
Fixed - line 649:
usage -1is a particular case ofusage != 0. I think this should this should be rewritten asif (this.usage -1 || this.usage > 0)to better reflect the two situations:<static> || <dynamic-in-use>. As it appears on line 627.
Replaced withisInUse()
- line 594: personally, I prefer
#18 Updated by Ovidiu Maxiniuc 3 months ago
I have no objections.
Constantin, I know I told you this week about some possible issues I seen while looking the first time at r16515 and newer, but after a second round my suspicions faded away. Sorry for the inconvenience.
#19 Updated by Constantin Asofiei 3 months ago
- Status changed from Review to Merge Pending
I've been testing this and is OK. I'm merging 11015b now.
#20 Updated by Constantin Asofiei 3 months ago
Branch 11015b was merged to trunk rev 16546 and archived.
#21 Updated by Constantin Asofiei 3 months ago
- Status changed from Merge Pending to Closed