| Line 3... |
Line 3... |
| 3 |
import com.spice.profitmandi.dao.model.ImeiActivationTimestampModel;
|
3 |
import com.spice.profitmandi.dao.model.ImeiActivationTimestampModel;
|
| 4 |
import com.spice.profitmandi.dao.repository.fofo.ActivatedImeiRepository;
|
4 |
import com.spice.profitmandi.dao.repository.fofo.ActivatedImeiRepository;
|
| 5 |
import org.apache.logging.log4j.LogManager;
|
5 |
import org.apache.logging.log4j.LogManager;
|
| 6 |
import org.apache.logging.log4j.Logger;
|
6 |
import org.apache.logging.log4j.Logger;
|
| 7 |
import org.springframework.beans.factory.annotation.Autowired;
|
7 |
import org.springframework.beans.factory.annotation.Autowired;
|
| - |
|
8 |
import com.smartdukaan.cron.monitored.ImeiActivationGauges;
|
| 8 |
import org.springframework.stereotype.Component;
|
9 |
import org.springframework.stereotype.Component;
|
| 9 |
|
10 |
|
| 10 |
import java.util.ArrayList;
|
11 |
import java.util.ArrayList;
|
| 11 |
import java.util.List;
|
12 |
import java.util.List;
|
| 12 |
import java.util.stream.Collectors;
|
13 |
import java.util.stream.Collectors;
|
| Line 24... |
Line 25... |
| 24 |
private MotorolaImeiActivationService motorolaImeiActivationService;
|
25 |
private MotorolaImeiActivationService motorolaImeiActivationService;
|
| 25 |
|
26 |
|
| 26 |
@Autowired
|
27 |
@Autowired
|
| 27 |
private ActivatedImeiRepository activatedImeiRepository;
|
28 |
private ActivatedImeiRepository activatedImeiRepository;
|
| 28 |
|
29 |
|
| - |
|
30 |
@Autowired
|
| - |
|
31 |
private ImeiActivationGauges gauges;
|
| - |
|
32 |
|
| 29 |
private static final Logger LOGGER = LogManager.getLogger(StandAlone.class);
|
33 |
private static final Logger LOGGER = LogManager.getLogger(StandAlone.class);
|
| 30 |
|
34 |
|
| - |
|
35 |
/**
|
| 31 |
public void checkOppoImeiStatus() throws Exception {
|
36 |
* ZERO, deliberately, and it is not an off-by-one.
|
| - |
|
37 |
*
|
| 32 |
List<ImeiActivationTimestampModel> imeiActivationTimestampModels = activatedImeiRepository.selectImeiActivationPendingByBrand("Oppo", 1, 25);
|
38 |
* The pool query keeps rows with createTimestamp < now().atStartOfDay().minusDays(DAYS).
|
| 33 |
List<String> imeis = imeiActivationTimestampModels.stream().map(x -> x.getSerialNumber()).collect(Collectors.toList());
|
39 |
* At DAYS=1 an imei answered today is measured against YESTERDAY midnight, so it is
|
| - |
|
40 |
* not due tomorrow either -- it comes back the day after, a two-day cadence. DAYS=0
|
| 34 |
LOGGER.info("Oppo secondary imeis {}", imeis);
|
41 |
* measures against this morning's midnight, which is what "a full run each day"
|
| 35 |
oppoImeiActivationService.updateActivationDate(imeis);
|
42 |
* actually means. Oppo and realme ran at 1 before this change.
|
| 36 |
}
|
43 |
*
|
| - |
|
44 |
* Within a pass the snapshot already guarantees one ask per imei, so this constant
|
| - |
|
45 |
* only governs the gap BETWEEN passes.
|
| - |
|
46 |
*/
|
| - |
|
47 |
private static final int DAYS = 0;
|
| 37 |
|
48 |
|
| 38 |
public void checkOppoImeiStatusTertiary() throws Exception {
|
49 |
/** Imeis per browser session. Recycles the driver; does NOT re-query the pool. */
|
| 39 |
List<ImeiActivationTimestampModel> imeiActivationTimestampModels = activatedImeiRepository.selectImeiActivationPendingByBrandTertiary("Oppo", 1, 25);
|
- |
|
| 40 |
List<String> imeis = imeiActivationTimestampModels.stream().map(x -> x.getSerialNumber()).collect(Collectors.toList());
|
- |
|
| 41 |
LOGGER.info("Oppo tertiary imeis {}", imeis);
|
- |
|
| 42 |
oppoImeiActivationService.updateActivationDate(imeis);
|
50 |
private static final int CHUNK = 25;
|
| 43 |
}
|
- |
|
| 44 |
|
51 |
|
| 45 |
public void checkRealmeImeiStatus() throws Exception {
|
52 |
/** Safety stop so a mis-set DAYS cannot pull an unbounded list into memory. */
|
| 46 |
List<ImeiActivationTimestampModel> imeiActivationTimestampModels = activatedImeiRepository.selectImeiActivationPendingByRealme("Realme", 1, 12);
|
- |
|
| 47 |
List<String> imeis = imeiActivationTimestampModels.stream().map(x -> x.getSerialNumber()).collect(Collectors.toList());
|
- |
|
| 48 |
LOGGER.info("Realme secondary imeis {}", imeis);
|
53 |
private static final int POOL_CAP = 10000;
|
| 49 |
realmeImeiActivationService.updateActivationDate(imeis);
|
- |
|
| 50 |
}
|
- |
|
| 51 |
|
54 |
|
| - |
|
55 |
/**
|
| 52 |
public void checkRealmeImeiStatusTertiary() throws Exception {
|
56 |
* Oppo and Realme share ONE thread and one work queue.
|
| - |
|
57 |
*
|
| - |
|
58 |
* Replaces the separate oppo() and realme() jobs. Nothing else in this class runs a
|
| - |
|
59 |
* browser, so with a single caller there is exactly one ChromeDriver alive at any
|
| - |
|
60 |
* moment -- down from four. Peak concurrent drivers is what triggers the OOM killer
|
| - |
|
61 |
* on this box, not average driver-seconds.
|
| - |
|
62 |
*
|
| - |
|
63 |
* ONE FULL PASS PER DAY, and no imei is asked twice inside a pass. Both pools are
|
| - |
|
64 |
* snapshotted before any browser starts and the pass walks those lists to the end;
|
| - |
|
65 |
* it never re-queries. That is the whole fix for the re-ask loop:
|
| - |
|
66 |
*
|
| - |
|
67 |
* A failed lookup writes no row (dateMap.put is only reached on the success path),
|
| 53 |
List<ImeiActivationTimestampModel> imeiActivationTimestampModels = activatedImeiRepository.selectImeiActivationPendingByBrandTertiary("Realme", 1, 12);
|
68 |
* so createTimestamp is not bumped, so the imei was still eligible on the next tick
|
| - |
|
69 |
* five minutes later. Measured 29-Aug: realme issued 4,524 requests against just
|
| - |
|
70 |
* 1,004 distinct imeis -- 4.5 asks each, 78% of the day's budget spent re-asking --
|
| - |
|
71 |
* while oppo, which rarely fails, sat at 1.03. More requests hardened the block,
|
| - |
|
72 |
* which caused more failures, which caused more requests.
|
| - |
|
73 |
*
|
| - |
|
74 |
* A snapshotted pass bounds that structurally: a failure costs one retry TOMORROW,
|
| - |
|
75 |
* never one in five minutes, no matter how the far end misbehaves.
|
| - |
|
76 |
*
|
| - |
|
77 |
* Sizing, measured on prod 2026-08-31 for a pass starting at midnight: oppo 4,133 and
|
| - |
|
78 |
* realme 2,118 imeis, which at 10.2s and 14.2s each is 11.7h + 8.4h = 20.1 hours of a
|
| - |
|
79 |
* single thread -- an 84% duty cycle. It fits, but there is NO slack. If per-imei
|
| - |
|
80 |
* seconds regress the pass will not finish and the tail rolls into the next day.
|
| - |
|
81 |
* Watch the "Daily pass finished" line: counts short of the "starting" line are the
|
| - |
|
82 |
* signal, and the lever is DAYS=1 (halves the work) rather than a second thread.
|
| - |
|
83 |
*
|
| - |
|
84 |
* Excluding stock billed today or yesterday is correctness, not capacity -- measured,
|
| 54 |
List<String> imeis = imeiActivationTimestampModels.stream().map(x -> x.getSerialNumber()).collect(Collectors.toList());
|
85 |
* it trims 30 imeis from oppo and 9 from realme. A handset billed in the last 48
|
| - |
|
86 |
* hours has essentially never been activated yet, and the cohort data agrees: the
|
| - |
|
87 |
* sold-within-180-days cohort returns a date on 2.4% (vivo) to 8.4% (oppo) of
|
| - |
|
88 |
* lookups, against 43-97% for stock sold over a year ago.
|
| - |
|
89 |
*
|
| - |
|
90 |
* ⚠ Realme's ceiling is a REQUEST-VOLUME ceiling, not a CPU one. realme.com stops
|
| - |
|
91 |
* serving the captcha widget as the day's request count climbs: measured 920/day ->
|
| - |
|
92 |
* 0.3% canvas timeouts, 3,467/day -> 28%, 4,524/day -> 75%, resetting at midnight --
|
| - |
|
93 |
* while oppo on the same box, same driver count, same widget vendor, at 4,350/day had
|
| - |
|
94 |
* ZERO timeouts across all 24 hours, on a box loaded at 0.9 of 6 cores. Realme's own
|
| - |
|
95 |
* canvas wait is 15s against oppo's 8s, so the longer wait is the one expiring. A
|
| - |
|
96 |
* daily pass puts realme near 2,127 requests/day, between the 920/day point where it
|
| - |
|
97 |
* was healthy and the 3,467/day point where it was 28% blocked -- so expect SOME
|
| - |
|
98 |
* blocking and judge the change on dates written, not on timeout count. What the
|
| - |
|
99 |
* pass guarantees is that blocking can no longer feed itself.
|
| - |
|
100 |
*
|
| - |
|
101 |
* Oppo and realme alternate chunk by chunk, so whichever pool still has work keeps
|
| - |
|
102 |
* the thread busy once the other is exhausted.
|
| - |
|
103 |
*/
|
| - |
|
104 |
public void checkBrowserImeiActivation() {
|
| - |
|
105 |
List<String> oppo = pendingFor("Oppo");
|
| - |
|
106 |
List<String> realme = pendingFor("Realme");
|
| - |
|
107 |
gauges.beginPass("Oppo", oppo.size());
|
| 55 |
LOGGER.info("Realme tertiary imeis {}", imeis);
|
108 |
gauges.beginPass("Realme", realme.size());
|
| - |
|
109 |
|
| - |
|
110 |
try {
|
| - |
|
111 |
if (oppo.isEmpty() && realme.isEmpty()) {
|
| - |
|
112 |
LOGGER.info("Oppo and Realme: nothing due today, not starting a browser");
|
| - |
|
113 |
return;
|
| - |
|
114 |
}
|
| - |
|
115 |
|
| - |
|
116 |
int oppoDone = 0;
|
| - |
|
117 |
int realmeDone = 0;
|
| - |
|
118 |
while (oppoDone < oppo.size() || realmeDone < realme.size()) {
|
| 56 |
realmeImeiActivationService.updateActivationDate(imeis);
|
119 |
oppoDone += runChunk("Oppo", oppo, oppoDone, oppoImeiActivationService::updateActivationDate);
|
| - |
|
120 |
realmeDone += runChunk("Realme", realme, realmeDone, realmeImeiActivationService::updateActivationDate);
|
| - |
|
121 |
}
|
| - |
|
122 |
} finally {
|
| - |
|
123 |
// In a finally so a pass killed part-way still publishes what it managed.
|
| - |
|
124 |
// A truncated pass is exactly the case worth alerting on, so it must not be
|
| - |
|
125 |
// the case that silently reports nothing.
|
| - |
|
126 |
gauges.endPass("Oppo");
|
| - |
|
127 |
gauges.endPass("Realme");
|
| - |
|
128 |
}
|
| 57 |
}
|
129 |
}
|
| 58 |
|
130 |
|
| 59 |
/**
|
131 |
/**
|
| - |
|
132 |
* One driver session's worth of one brand, taken from a list that was snapshotted
|
| 60 |
* Secondary + tertiary in ONE browser session per brand.
|
133 |
* before the pass began. Returns how many were consumed so the caller can advance.
|
| 61 |
*
|
134 |
*
|
| 62 |
* They were separate @Scheduled jobs, so each brand span up two ChromeDrivers at
|
- |
|
| 63 |
* once and the fleet ran four. Each driver tree costs roughly 850MB, and this box
|
135 |
* Chunking exists only to recycle the browser -- a driver held open for the whole
|
| 64 |
* co-hosts a 9.4GB tomcat with ~3GB available -- it has been OOM-killed twice this
|
136 |
* pass would leak memory on a box that has been OOM-killed twice, and a crash would
|
| 65 |
* month and tomcat was the victim both times. Halving concurrent drivers is the
|
137 |
* cost the entire pool rather than CHUNK imeis. It deliberately does NOT re-query:
|
| 66 |
* single biggest memory saving available here.
|
- |
|
| 67 |
*
|
- |
|
| 68 |
* Nothing else changes: the two pools differ only in which named query fills them,
|
138 |
* re-querying between chunks is what produced the 4.5 lookups per imei per day that
|
| 69 |
* and both already funnel into the same updateActivationDate -> checkWarranty ->
|
- |
|
| 70 |
* saveActivation path. The pools are disjoint by construction (secondary excludes
|
- |
|
| 71 |
* anything with a FofoLineItem, tertiary is FofoLineItem-based); distinct() is cheap
|
- |
|
| 72 |
* insurance rather than a fix for a known overlap.
|
- |
|
| 73 |
*
|
- |
|
| 74 |
*
|
- |
|
| 75 |
* Sizing: merging SERIALISES work that used to run in parallel, so keeping the old
|
- |
|
| 76 |
* batch sizes would cost throughput. Measured post-cap at 10.2s/imei (oppo) and
|
- |
|
| 77 |
* 14.2s/imei (realme), solving M * 86400 / (300 + M*t):
|
- |
|
| 78 |
* oppo 2 parallel jobs x10 = 4,299/day -> merged 30 = 4,277/day (parity)
|
- |
|
| 79 |
* realme 2 parallel jobs x10 = 3,910/day -> merged 20 = 2,959/day (-24%)
|
- |
|
| 80 |
* Oppo is sized to hold parity because it is already short of its 4,798/day need.
|
- |
|
| 81 |
* Realme is left at 20 -- it needs 2,243/day, so it can absorb the dip in exchange
|
- |
|
| 82 |
* for shorter batches.
|
139 |
* this rewrite removes.
|
| 83 |
*
|
- |
|
| 84 |
* Note what this does and does not save: total driver-SECONDS are unchanged (that is
|
- |
|
| 85 |
* the point of resizing), but PEAK concurrent drivers halves from 4 to 2. Peak is
|
- |
|
| 86 |
* what triggers the OOM killer, not average.
|
- |
|
| 87 |
*
|
- |
|
| 88 |
* The per-pool log lines are kept so existing log analysis still works.
|
- |
|
| 89 |
*/
|
140 |
*/
|
| 90 |
public void checkOppoImeiStatusCombined() throws Exception {
|
141 |
private int runChunk(String brand, List<String> pool, int from, ImeiBatch batch) {
|
| 91 |
List<String> secondary = activatedImeiRepository.selectImeiActivationPendingByBrand("Oppo", 1, 15)
|
- |
|
| 92 |
.stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList());
|
- |
|
| 93 |
List<String> tertiary = activatedImeiRepository.selectImeiActivationPendingByBrandTertiary("Oppo", 1, 15)
|
- |
|
| 94 |
.stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList());
|
- |
|
| 95 |
LOGGER.info("Oppo secondary imeis {}", secondary);
|
- |
|
| 96 |
LOGGER.info("Oppo tertiary imeis {}", tertiary);
|
- |
|
| 97 |
List<String> all = new ArrayList<>(secondary);
|
- |
|
| 98 |
all.addAll(tertiary);
|
- |
|
| 99 |
all = all.stream().distinct().collect(Collectors.toList());
|
- |
|
| 100 |
if (all.isEmpty()) {
|
142 |
if (from >= pool.size()) {
|
| 101 |
LOGGER.info("Oppo: nothing pending, not starting a browser");
|
- |
|
| 102 |
return;
|
143 |
return 0;
|
| 103 |
}
|
144 |
}
|
| - |
|
145 |
List<String> chunk = pool.subList(from, Math.min(from + CHUNK, pool.size()));
|
| - |
|
146 |
runBrand(brand, chunk, batch);
|
| 104 |
oppoImeiActivationService.updateActivationDate(all);
|
147 |
gauges.churned(brand, chunk.size());
|
| - |
|
148 |
return chunk.size();
|
| 105 |
}
|
149 |
}
|
| 106 |
|
150 |
|
| 107 |
public void checkRealmeImeiStatusCombined() throws Exception {
|
- |
|
| 108 |
List<String> secondary = activatedImeiRepository.selectImeiActivationPendingByRealme("Realme", 1, 12)
|
- |
|
| - |
|
151 |
/**
|
| 109 |
.stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList());
|
152 |
* One brand's turn. Wrapped so a failure in the first brand still lets the second
|
| 110 |
List<String> tertiary = activatedImeiRepository.selectImeiActivationPendingByBrandTertiary("Realme", 1, 12)
|
153 |
* one run -- these are separate sites and separate driver sessions, and a realme
|
| 111 |
.stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList());
|
- |
|
| 112 |
LOGGER.info("Realme secondary imeis {}", secondary);
|
- |
|
| 113 |
LOGGER.info("Realme tertiary imeis {}", tertiary);
|
- |
|
| 114 |
List<String> all = new ArrayList<>(secondary);
|
154 |
* outage must not cost oppo its whole tick.
|
| 115 |
all.addAll(tertiary);
|
155 |
*/
|
| 116 |
all = all.stream().distinct().collect(Collectors.toList());
|
156 |
private void runBrand(String brand, List<String> imeis, ImeiBatch batch) {
|
| 117 |
if (all.isEmpty()) {
|
157 |
if (imeis.isEmpty()) {
|
| 118 |
LOGGER.info("Realme: nothing pending, not starting a browser");
|
- |
|
| 119 |
return;
|
158 |
return;
|
| 120 |
}
|
159 |
}
|
| - |
|
160 |
LOGGER.info("{} imeis {}", brand, imeis);
|
| - |
|
161 |
try {
|
| - |
|
162 |
batch.run(imeis);
|
| - |
|
163 |
} catch (Exception e) {
|
| - |
|
164 |
gauges.error(brand);
|
| - |
|
165 |
LOGGER.error("{} activation batch failed, continuing with the next brand", brand, e);
|
| - |
|
166 |
}
|
| - |
|
167 |
}
|
| - |
|
168 |
|
| - |
|
169 |
@FunctionalInterface
|
| - |
|
170 |
private interface ImeiBatch {
|
| - |
|
171 |
void run(List<String> imeis) throws Exception;
|
| - |
|
172 |
}
|
| - |
|
173 |
|
| - |
|
174 |
/**
|
| - |
|
175 |
* Everything due for one brand today, as a single list.
|
| - |
|
176 |
*
|
| - |
|
177 |
* The secondary/tertiary split is an artefact of there being two join paths to a
|
| - |
|
178 |
* serial (transaction.lineitem vs fofo.fofo_line_item), not two kinds of work: both
|
| - |
|
179 |
* funnel into the same updateActivationDate -> checkWarranty -> saveActivation path.
|
| - |
|
180 |
* They were separate jobs with separate batch sizes, which is what made the split
|
| - |
|
181 |
* visible at all. A pass walks the whole list to the end, so nothing can be starved
|
| - |
|
182 |
* by ordering and a plain concatenation is enough.
|
| - |
|
183 |
*
|
| - |
|
184 |
* The pools are disjoint by construction -- the secondary query excludes anything
|
| - |
|
185 |
* with a FofoLineItem -- so distinct() is cheap insurance, not a fix for a known
|
| 121 |
realmeImeiActivationService.updateActivationDate(all);
|
186 |
* overlap. selectImeiActivationPendingByRealme is a verbatim duplicate of
|
| - |
|
187 |
* selectImeiActivationPendingByBrand (same named query, same parameters), so the
|
| - |
|
188 |
* brand-generic call is used for every brand.
|
| - |
|
189 |
*/
|
| - |
|
190 |
private List<String> pendingFor(String brand) {
|
| - |
|
191 |
List<String> pool = new ArrayList<>();
|
| - |
|
192 |
pool.addAll(activatedImeiRepository.selectImeiActivationPendingByBrand(brand, DAYS, POOL_CAP)
|
| - |
|
193 |
.stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList()));
|
| - |
|
194 |
pool.addAll(activatedImeiRepository.selectImeiActivationPendingByBrandTertiary(brand, DAYS, POOL_CAP)
|
| - |
|
195 |
.stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList()));
|
| - |
|
196 |
if (pool.size() >= POOL_CAP) {
|
| - |
|
197 |
LOGGER.warn("{} pool hit the {} cap -- the pass will not cover everything due today", brand, POOL_CAP);
|
| - |
|
198 |
}
|
| - |
|
199 |
return pool.stream().distinct().collect(Collectors.toList());
|
| 122 |
}
|
200 |
}
|
| 123 |
|
201 |
|
| 124 |
/**
|
202 |
/**
|
| 125 |
* Motorola: secondary + tertiary in ONE browser session, same shape as the
|
203 |
* Motorola: secondary + tertiary in ONE browser session, same shape as the
|
| 126 |
* oppo/realme combined jobs.
|
204 |
* oppo/realme combined jobs.
|