Subversion Repositories SmartDukaan

Rev

Rev 37472 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
30308 amit.gupta 1
package com.smartdukaan.cron.scheduled;
2
 
3
import com.spice.profitmandi.dao.model.ImeiActivationTimestampModel;
4
import com.spice.profitmandi.dao.repository.fofo.ActivatedImeiRepository;
30353 amit.gupta 5
import org.apache.logging.log4j.LogManager;
6
import org.apache.logging.log4j.Logger;
30308 amit.gupta 7
import org.springframework.beans.factory.annotation.Autowired;
37472 amit 8
import com.smartdukaan.cron.monitored.ImeiActivationGauges;
30308 amit.gupta 9
import org.springframework.stereotype.Component;
10
 
37447 amit 11
import java.util.ArrayList;
30308 amit.gupta 12
import java.util.List;
13
import java.util.stream.Collectors;
14
 
15
@Component
16
public class StandAlone {
17
 
18
	@Autowired
30352 amit.gupta 19
	private OppoImeiActivationService oppoImeiActivationService;
30308 amit.gupta 20
 
21
	@Autowired
34418 amit.gupta 22
	private RealmeImeiActivationService realmeImeiActivationService;
23
 
24
	@Autowired
37459 amit 25
	private MotorolaImeiActivationService motorolaImeiActivationService;
26
 
27
	@Autowired
30352 amit.gupta 28
	private ActivatedImeiRepository activatedImeiRepository;
30308 amit.gupta 29
 
37472 amit 30
	@Autowired
31
	private ImeiActivationGauges gauges;
32
 
30353 amit.gupta 33
	private static final Logger LOGGER = LogManager.getLogger(StandAlone.class);
34
 
37472 amit 35
	/**
36
	 * ZERO, deliberately, and it is not an off-by-one.
37
	 *
38
	 * The pool query keeps rows with createTimestamp < now().atStartOfDay().minusDays(DAYS).
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
41
	 * measures against this morning's midnight, which is what "a full run each day"
42
	 * actually means. Oppo and realme ran at 1 before this change.
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;
34418 amit.gupta 48
 
37472 amit 49
	/** Imeis per browser session. Recycles the driver; does NOT re-query the pool. */
50
	private static final int CHUNK = 25;
36253 amit 51
 
37472 amit 52
	/** Safety stop so a mis-set DAYS cannot pull an unbounded list into memory. */
53
	private static final int POOL_CAP = 10000;
36253 amit 54
 
37447 amit 55
	/**
37472 amit 56
	 * Oppo and Realme share ONE thread and one work queue.
37447 amit 57
	 *
37472 amit 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.
37447 amit 62
	 *
37472 amit 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:
37447 amit 66
	 *
37472 amit 67
	 *   A failed lookup writes no row (dateMap.put is only reached on the success path),
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.
37447 amit 73
	 *
37472 amit 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.
37447 amit 76
	 *
37472 amit 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.
37447 amit 83
	 *
37472 amit 84
	 * Excluding stock billed today or yesterday is correctness, not capacity -- measured,
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.
37447 amit 103
	 */
37472 amit 104
	public void checkBrowserImeiActivation() {
105
		List<String> oppo = pendingFor("Oppo");
106
		List<String> realme = pendingFor("Realme");
107
		gauges.beginPass("Oppo", oppo.size());
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()) {
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");
37447 amit 128
		}
129
	}
130
 
37472 amit 131
	/**
132
	 * One driver session's worth of one brand, taken from a list that was snapshotted
133
	 * before the pass began. Returns how many were consumed so the caller can advance.
134
	 *
135
	 * Chunking exists only to recycle the browser -- a driver held open for the whole
136
	 * pass would leak memory on a box that has been OOM-killed twice, and a crash would
137
	 * cost the entire pool rather than CHUNK imeis. It deliberately does NOT re-query:
138
	 * re-querying between chunks is what produced the 4.5 lookups per imei per day that
139
	 * this rewrite removes.
140
	 */
141
	private int runChunk(String brand, List<String> pool, int from, ImeiBatch batch) {
142
		if (from >= pool.size()) {
143
			return 0;
144
		}
145
		List<String> chunk = pool.subList(from, Math.min(from + CHUNK, pool.size()));
146
		runBrand(brand, chunk, batch);
147
		gauges.churned(brand, chunk.size());
148
		return chunk.size();
149
	}
150
 
151
	/**
152
	 * One brand's turn. Wrapped so a failure in the first brand still lets the second
153
	 * one run -- these are separate sites and separate driver sessions, and a realme
154
	 * outage must not cost oppo its whole tick.
155
	 */
156
	private void runBrand(String brand, List<String> imeis, ImeiBatch batch) {
157
		if (imeis.isEmpty()) {
37447 amit 158
			return;
159
		}
37472 amit 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
		}
37447 amit 167
	}
168
 
37472 amit 169
	@FunctionalInterface
170
	private interface ImeiBatch {
171
		void run(List<String> imeis) throws Exception;
172
	}
173
 
37459 amit 174
	/**
37472 amit 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
37482 amit 186
	 * overlap. The brand-generic call is used for every brand; the realme-named copy of
187
	 * it has been deleted, it was a verbatim duplicate down to the named query.
37472 amit 188
	 */
189
	private List<String> pendingFor(String brand) {
190
		List<String> pool = new ArrayList<>();
191
		pool.addAll(activatedImeiRepository.selectImeiActivationPendingByBrand(brand, DAYS, POOL_CAP)
192
				.stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList()));
193
		pool.addAll(activatedImeiRepository.selectImeiActivationPendingByBrandTertiary(brand, DAYS, POOL_CAP)
194
				.stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList()));
195
		if (pool.size() >= POOL_CAP) {
196
			LOGGER.warn("{} pool hit the {} cap -- the pass will not cover everything due today", brand, POOL_CAP);
197
		}
198
		return pool.stream().distinct().collect(Collectors.toList());
199
	}
200
 
201
	/**
37459 amit 202
	 * Motorola: secondary + tertiary in ONE browser session, same shape as the
203
	 * oppo/realme combined jobs.
204
	 *
205
	 * Cadence: the pool query defers an imei for `days` after each attempt
206
	 * (saveActivation bumps createTimestamp even when no date came back), so
207
	 * days=2 gives the requested "retry everything every two days".
208
	 *
209
	 * Sizing: the pending pool measured 1,534 (1,182 secondary + 352 tertiary).
210
	 * At the ~10-14s/imei the oppo and realme jobs measure, 60 per invocation is
211
	 * roughly 12 minutes of driver time, and clearing 1,534 inside 48h needs
212
	 * about 26 invocations -- i.e. an OS cron entry every 90 minutes, with
213
	 * headroom. Do NOT schedule it inside the oppo/realme window: each driver
214
	 * tree costs ~850MB and this box has been OOM-killed twice with tomcat the
215
	 * victim, so peak concurrent drivers is the number that matters.
216
	 */
217
	public void checkMotorolaImeiStatusCombined() throws Exception {
218
		List<String> secondary = activatedImeiRepository.selectImeiActivationPendingByBrand("Motorola", 2, 30)
219
				.stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList());
220
		List<String> tertiary = activatedImeiRepository.selectImeiActivationPendingByBrandTertiary("Motorola", 2, 30)
221
				.stream().map(ImeiActivationTimestampModel::getSerialNumber).collect(Collectors.toList());
222
		LOGGER.info("Motorola secondary imeis {}", secondary);
223
		LOGGER.info("Motorola tertiary imeis {}", tertiary);
224
		List<String> all = new ArrayList<>(secondary);
225
		all.addAll(tertiary);
226
		all = all.stream().distinct().collect(Collectors.toList());
227
		if (all.isEmpty()) {
228
			LOGGER.info("Motorola: nothing pending, not starting a browser");
229
			return;
230
		}
231
		motorolaImeiActivationService.updateActivationDate(all);
232
	}
233
 
36253 amit 234
}