Subversion Repositories SmartDukaan

Rev

Rev 37459 | Rev 37482 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 37459 Rev 37472
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.