Rev 37472 | Blame | Compare with Previous | Last modification | View Log | RSS feed
package com.smartdukaan.cron.scheduled;import com.spice.profitmandi.dao.model.ImeiActivationTimestampModel;import com.spice.profitmandi.dao.repository.fofo.ActivatedImeiRepository;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import com.smartdukaan.cron.monitored.ImeiActivationGauges;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;import java.util.stream.Collectors;@Componentpublic class StandAlone {@Autowiredprivate OppoImeiActivationService oppoImeiActivationService;@Autowiredprivate RealmeImeiActivationService realmeImeiActivationService;@Autowiredprivate MotorolaImeiActivationService motorolaImeiActivationService;@Autowiredprivate ActivatedImeiRepository activatedImeiRepository;@Autowiredprivate ImeiActivationGauges gauges;private static final Logger LOGGER = LogManager.getLogger(StandAlone.class);/*** ZERO, deliberately, and it is not an off-by-one.** The pool query keeps rows with createTimestamp < now().atStartOfDay().minusDays(DAYS).* At DAYS=1 an imei answered today is measured against YESTERDAY midnight, so it is* not due tomorrow either -- it comes back the day after, a two-day cadence. DAYS=0* measures against this morning's midnight, which is what "a full run each day"* actually means. Oppo and realme ran at 1 before this change.** Within a pass the snapshot already guarantees one ask per imei, so this constant* only governs the gap BETWEEN passes.*/private static final int DAYS = 0;/** Imeis per browser session. Recycles the driver; does NOT re-query the pool. */private static final int CHUNK = 25;/** Safety stop so a mis-set DAYS cannot pull an unbounded list into memory. */private static final int POOL_CAP = 10000;/*** Oppo and Realme share ONE thread and one work queue.** Replaces the separate oppo() and realme() jobs. Nothing else in this class runs a* browser, so with a single caller there is exactly one ChromeDriver alive at any* moment -- down from four. Peak concurrent drivers is what triggers the OOM killer* on this box, not average driver-seconds.** ONE FULL PASS PER DAY, and no imei is asked twice inside a pass. Both pools are* snapshotted before any browser starts and the pass walks those lists to the end;* it never re-queries. That is the whole fix for the re-ask loop:** A failed lookup writes no row (dateMap.put is only reached on the success path),* so createTimestamp is not bumped, so the imei was still eligible on the next tick* five minutes later. Measured 29-Aug: realme issued 4,524 requests against just* 1,004 distinct imeis -- 4.5 asks each, 78% of the day's budget spent re-asking --* while oppo, which rarely fails, sat at 1.03. More requests hardened the block,* which caused more failures, which caused more requests.** A snapshotted pass bounds that structurally: a failure costs one retry TOMORROW,* never one in five minutes, no matter how the far end misbehaves.** Sizing, measured on prod 2026-08-31 for a pass starting at midnight: oppo 4,133 and* realme 2,118 imeis, which at 10.2s and 14.2s each is 11.7h + 8.4h = 20.1 hours of a* single thread -- an 84% duty cycle. It fits, but there is NO slack. If per-imei* seconds regress the pass will not finish and the tail rolls into the next day.* Watch the "Daily pass finished" line: counts short of the "starting" line are the* signal, and the lever is DAYS=1 (halves the work) rather than a second thread.** Excluding stock billed today or yesterday is correctness, not capacity -- measured,* it trims 30 imeis from oppo and 9 from realme. A handset billed in the last 48* hours has essentially never been activated yet, and the cohort data agrees: the* sold-within-180-days cohort returns a date on 2.4% (vivo) to 8.4% (oppo) of* lookups, against 43-97% for stock sold over a year ago.** ⚠ Realme's ceiling is a REQUEST-VOLUME ceiling, not a CPU one. realme.com stops* serving the captcha widget as the day's request count climbs: measured 920/day ->* 0.3% canvas timeouts, 3,467/day -> 28%, 4,524/day -> 75%, resetting at midnight --* while oppo on the same box, same driver count, same widget vendor, at 4,350/day had* ZERO timeouts across all 24 hours, on a box loaded at 0.9 of 6 cores. Realme's own* canvas wait is 15s against oppo's 8s, so the longer wait is the one expiring. A* daily pass puts realme near 2,127 requests/day, between the 920/day point where it* was healthy and the 3,467/day point where it was 28% blocked -- so expect SOME* blocking and judge the change on dates written, not on timeout count. What the* pass guarantees is that blocking can no longer feed itself.** Oppo and realme alternate chunk by chunk, so whichever pool still has work keeps* the thread busy once the other is exhausted.*/public void checkBrowserImeiActivation() {List<String> oppo = pendingFor("Oppo");List<String> realme = pendingFor("Realme");gauges.beginPass("Oppo", oppo.size());gauges.beginPass("Realme", realme.size());try {if (oppo.isEmpty() && realme.isEmpty()) {LOGGER.info("Oppo and Realme: nothing due today, not starting a browser");return;}int oppoDone = 0;int realmeDone = 0;while (oppoDone < oppo.size() || realmeDone < realme.size()) {oppoDone += runChunk("Oppo", oppo, oppoDone, oppoImeiActivationService::updateActivationDate);realmeDone += runChunk("Realme", realme, realmeDone, realmeImeiActivationService::updateActivationDate);}} finally {// In a finally so a pass killed part-way still publishes what it managed.// A truncated pass is exactly the case worth alerting on, so it must not be// the case that silently reports nothing.gauges.endPass("Oppo");gauges.endPass("Realme");}}/*** One driver session's worth of one brand, taken from a list that was snapshotted* before the pass began. Returns how many were consumed so the caller can advance.** Chunking exists only to recycle the browser -- a driver held open for the whole* pass would leak memory on a box that has been OOM-killed twice, and a crash would* cost the entire pool rather than CHUNK imeis. It deliberately does NOT re-query:* re-querying between chunks is what produced the 4.5 lookups per imei per day that* this rewrite removes.*/private int runChunk(String brand, List<String> pool, int from, ImeiBatch batch) {if (from >= pool.size()) {return 0;}List<String> chunk = pool.subList(from, Math.min(from + CHUNK, pool.size()));runBrand(brand, chunk, batch);gauges.churned(brand, chunk.size());return chunk.size();}/*** One brand's turn. Wrapped so a failure in the first brand still lets the second* one run -- these are separate sites and separate driver sessions, and a realme* outage must not cost oppo its whole tick.*/private void runBrand(String brand, List<String> imeis, ImeiBatch batch) {if (imeis.isEmpty()) {return;}LOGGER.info("{} imeis {}", brand, imeis);try {batch.run(imeis);} catch (Exception e) {gauges.error(brand);LOGGER.error("{} activation batch failed, continuing with the next brand", brand, e);}}@FunctionalInterfaceprivate interface ImeiBatch {void run(List<String> imeis) throws Exception;}/*** Everything due for one brand today, as a single list.** The secondary/tertiary split is an artefact of there being two join paths to a* serial (transaction.lineitem vs fofo.fofo_line_item), not two kinds of work: both* funnel into the same updateActivationDate -> checkWarranty -> saveActivation path.* They were separate jobs with separate batch sizes, which is what made the split* visible at all. A pass walks the whole list to the end, so nothing can be starved* by ordering and a plain concatenation is enough.** The pools are disjoint by construction -- the secondary query excludes anything* with a FofoLineItem -- so distinct() is cheap insurance, not a fix for a known* overlap. The brand-generic call is used for every brand; the realme-named copy of* it has been deleted, it was a verbatim duplicate down to the named query.*/private List<String> pendingFor(String brand) {List<String> pool = new ArrayList<>();pool.addAll(activatedImeiRepository.selectImeiActivationPendingByBrand(brand, DAYS, POOL_CAP).stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList()));pool.addAll(activatedImeiRepository.selectImeiActivationPendingByBrandTertiary(brand, DAYS, POOL_CAP).stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList()));if (pool.size() >= POOL_CAP) {LOGGER.warn("{} pool hit the {} cap -- the pass will not cover everything due today", brand, POOL_CAP);}return pool.stream().distinct().collect(Collectors.toList());}/*** Motorola: secondary + tertiary in ONE browser session, same shape as the* oppo/realme combined jobs.** Cadence: the pool query defers an imei for `days` after each attempt* (saveActivation bumps createTimestamp even when no date came back), so* days=2 gives the requested "retry everything every two days".** Sizing: the pending pool measured 1,534 (1,182 secondary + 352 tertiary).* At the ~10-14s/imei the oppo and realme jobs measure, 60 per invocation is* roughly 12 minutes of driver time, and clearing 1,534 inside 48h needs* about 26 invocations -- i.e. an OS cron entry every 90 minutes, with* headroom. Do NOT schedule it inside the oppo/realme window: each driver* tree costs ~850MB and this box has been OOM-killed twice with tomcat the* victim, so peak concurrent drivers is the number that matters.*/public void checkMotorolaImeiStatusCombined() throws Exception {List<String> secondary = activatedImeiRepository.selectImeiActivationPendingByBrand("Motorola", 2, 30).stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList());List<String> tertiary = activatedImeiRepository.selectImeiActivationPendingByBrandTertiary("Motorola", 2, 30).stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList());LOGGER.info("Motorola secondary imeis {}", secondary);LOGGER.info("Motorola tertiary imeis {}", tertiary);List<String> all = new ArrayList<>(secondary);all.addAll(tertiary);all = all.stream().distinct().collect(Collectors.toList());if (all.isEmpty()) {LOGGER.info("Motorola: nothing pending, not starting a browser");return;}motorolaImeiActivationService.updateActivationDate(all);}}