When a group is unmarked as provisionable for a provisioner and was never actually provisioned to the target, its sync group row and associated sync membership rows were never cleaned up. The timeout-based cleanup required inTargetEnd to be non-null, but for groups that were never in the target, inTargetEnd is always null, so the cleanup condition was never satisfied. Over time this causes sync tables to accumulate stale rows, which degrades incremental provisioning performance (e.g. Harvard Okta provisioner taking 57 minutes for 3 inserts).
Changes:
1. Removed the inTargetEnd != null guard from the timeout cleanup in fullSyncGroups, fullSyncMembers, and fullSyncMemberships in GrouperProvisioningSyncIntegration. The inner logic already handles null inTargetEnd gracefully by using max(inTargetEnd, provisionableEnd, lastUpdated) to determine the timeout start.
2. Added a per-provisioner advanced config removeSyncRowsAfterSecondsOutOfTarget so individual provisioners can override the global timeout. Falls back to grouper.provisioning.removeSyncRowsAfterSecondsOutOfTarget (default 604800 seconds / 7 days).
3. In filterByGroupNotProvisionable, skip groups that are not provisionable and not in the target, avoiding unnecessary sync membership queries that were the main cause of the performance degradation.
4. Added stale sync row cleanup to the MAINTENANCE__deleteOldRecords daemon via deleteOldStaleSyncRows(). For each active provisioner, queries for sync groups, members, and memberships that are no longer provisionable/in-target and whose timestamps are past the cutoff. Uses 2x the configured timeout (minimum 2 weeks) to avoid conflicting with the provisioner's own cleanup. Each provisioner is wrapped in try/catch so one failure doesn't stop the others. Deletes via DAO which cascades to memberships and logs.