| 37672 |
amit |
1 |
package com.spice.profitmandi.common.util;
|
|
|
2 |
|
|
|
3 |
import java.io.Closeable;
|
|
|
4 |
import java.io.File;
|
|
|
5 |
import java.io.IOException;
|
|
|
6 |
import java.io.RandomAccessFile;
|
|
|
7 |
import java.nio.channels.FileChannel;
|
|
|
8 |
import java.nio.channels.FileLock;
|
|
|
9 |
import java.nio.channels.OverlappingFileLockException;
|
|
|
10 |
import java.nio.file.Files;
|
|
|
11 |
import java.nio.file.Paths;
|
|
|
12 |
import java.nio.file.attribute.PosixFilePermission;
|
|
|
13 |
import java.util.EnumSet;
|
|
|
14 |
import java.util.Set;
|
|
|
15 |
import java.util.concurrent.TimeUnit;
|
|
|
16 |
import java.util.concurrent.locks.ReentrantLock;
|
|
|
17 |
|
|
|
18 |
import org.apache.logging.log4j.LogManager;
|
|
|
19 |
import org.apache.logging.log4j.Logger;
|
|
|
20 |
|
|
|
21 |
/**
|
|
|
22 |
* A machine-wide mutex around "a ChromeDriver tree is running right now".
|
|
|
23 |
*
|
|
|
24 |
* <p>Every headless chrome we start costs roughly 850MB RSS across its process tree. The
|
|
|
25 |
* app box holds a -Xmx8g tomcat and a -Xmx2g cron jar on 16GB and has been kernel-OOM-killed
|
|
|
26 |
* twice, tomcat the victim both times, so the number that matters is PEAK CONCURRENT DRIVERS,
|
|
|
27 |
* not drivers per day.
|
|
|
28 |
*
|
|
|
29 |
* <p>Measured 2026-09-17 on the oppo/realme lane: a 25-imei chunk takes 505s (12s of driver
|
|
|
30 |
* startup, then ~19.7s an imei) and the lane idles for the 20s fixedDelay before the next
|
|
|
31 |
* chunk. That is a <b>96% duty cycle</b> -- a driver is alive almost always. Meanwhile tomcat
|
|
|
32 |
* starts its own driver 8 times a day for the knowlarity scrape. On 2026-09-17 that scrape ran
|
|
|
33 |
* 11:40:00-11:41:07 and the lane's next driver came up at 11:42:21: it cleared by 74 seconds,
|
|
|
34 |
* by luck. At a 96% duty cycle you cannot dodge the collision by choosing better times, so the
|
|
|
35 |
* only fix is to serialise.
|
|
|
36 |
*
|
|
|
37 |
* <p>The two JVMs are separate processes owned by different users (cron as root, tomcat as
|
|
|
38 |
* tomcat), so an in-process flag like KnowlarityInsightsService's {@code isScraping} cannot see
|
|
|
39 |
* across the boundary. This uses an OS file lock, which the kernel releases when the holder
|
|
|
40 |
* dies -- a jar that gets OOM-killed mid-chunk does not strand the lane, which a marker file
|
|
|
41 |
* would.
|
|
|
42 |
*
|
|
|
43 |
* <p>Callers WAIT rather than skip. Waiting is cheap and skipping is not: the knowlarity scrape
|
|
|
44 |
* needs 67s against a lane that is busy 96% of the time, so a skip-on-contention policy would
|
|
|
45 |
* mean it essentially never refreshed again. Eight scrapes a day costs the imei lane about nine
|
|
|
46 |
* minutes of waiting in total, which is inside the noise of a lane that already loses whole
|
|
|
47 |
* chunks to captcha refusals.
|
|
|
48 |
*
|
|
|
49 |
* <pre>
|
|
|
50 |
* try (BrowserLane lane = BrowserLane.acquire("oppo", 5, TimeUnit.MINUTES)) {
|
|
|
51 |
* if (lane == null) {
|
|
|
52 |
* return; // lane stayed busy; caller leaves its work pending
|
|
|
53 |
* }
|
|
|
54 |
* driver = new ChromeDriver(options);
|
|
|
55 |
* ...
|
|
|
56 |
* } finally {
|
|
|
57 |
* driver.quit();
|
|
|
58 |
* }
|
|
|
59 |
* </pre>
|
|
|
60 |
*
|
|
|
61 |
* <p>⚠ Acquire BEFORE {@code new ChromeDriver(...)} and hold until after {@code quit()} has
|
|
|
62 |
* returned. Holding only around the constructor protects nothing -- the 850MB is resident for
|
|
|
63 |
* the whole chunk, not just at startup.
|
|
|
64 |
*/
|
|
|
65 |
public final class BrowserLane implements Closeable {
|
|
|
66 |
|
|
|
67 |
private static final Logger LOGGER = LogManager.getLogger(BrowserLane.class);
|
|
|
68 |
|
|
|
69 |
/**
|
|
|
70 |
* Overridable so a dev box (macOS has no /var/lock) or a test can point somewhere writable.
|
|
|
71 |
*/
|
|
|
72 |
private static final String LOCK_PATH = System.getProperty("sd.browser.lock", "/var/lock/sd-browser.lock");
|
|
|
73 |
|
|
|
74 |
/**
|
|
|
75 |
* The file lock is per-JVM, not per-thread: a second thread in the SAME jvm asking for a
|
|
|
76 |
* lock this jvm already holds gets an OverlappingFileLockException rather than blocking.
|
|
|
77 |
* This gate makes threads queue first so only one of them ever reaches the file lock.
|
|
|
78 |
* Fair, so a waiter cannot be starved by the lane's own 20s-cadence ticks.
|
|
|
79 |
*/
|
|
|
80 |
private static final ReentrantLock IN_JVM = new ReentrantLock(true);
|
|
|
81 |
|
|
|
82 |
private static final long POLL_MILLIS = 2000L;
|
|
|
83 |
|
|
|
84 |
private final String owner;
|
|
|
85 |
private final RandomAccessFile handle;
|
|
|
86 |
private final FileLock fileLock;
|
|
|
87 |
private boolean released;
|
|
|
88 |
|
|
|
89 |
private BrowserLane(String owner, RandomAccessFile handle, FileLock fileLock) {
|
|
|
90 |
this.owner = owner;
|
|
|
91 |
this.handle = handle;
|
|
|
92 |
this.fileLock = fileLock;
|
|
|
93 |
}
|
|
|
94 |
|
|
|
95 |
/**
|
|
|
96 |
* Waits for the browser lane, up to the given timeout.
|
|
|
97 |
*
|
|
|
98 |
* @param owner what to name in the logs -- the brand or job asking
|
|
|
99 |
* @return the held lane, or {@code null} if it stayed busy for the whole timeout, in which
|
|
|
100 |
* case the caller must NOT start a driver
|
|
|
101 |
*/
|
|
|
102 |
public static BrowserLane acquire(String owner, long timeout, TimeUnit unit) {
|
|
|
103 |
long deadline = System.currentTimeMillis() + unit.toMillis(timeout);
|
|
|
104 |
|
|
|
105 |
boolean gated;
|
|
|
106 |
try {
|
|
|
107 |
gated = IN_JVM.tryLock(unit.toMillis(timeout), TimeUnit.MILLISECONDS);
|
|
|
108 |
} catch (InterruptedException e) {
|
|
|
109 |
Thread.currentThread().interrupt();
|
|
|
110 |
LOGGER.warn("[{}] interrupted waiting for the in-jvm browser gate", owner);
|
|
|
111 |
return null;
|
|
|
112 |
}
|
|
|
113 |
if (!gated) {
|
|
|
114 |
LOGGER.warn("[{}] browser lane still held by this jvm after {} {} - not starting a driver",
|
|
|
115 |
owner, timeout, unit);
|
|
|
116 |
return null;
|
|
|
117 |
}
|
|
|
118 |
|
|
|
119 |
RandomAccessFile handle = null;
|
|
|
120 |
try {
|
|
|
121 |
handle = open();
|
|
|
122 |
} catch (Exception e) {
|
|
|
123 |
// A missing /var/lock, a read-only mount, a dev laptop. Serialising within this jvm
|
|
|
124 |
// is still better than nothing, and refusing to run would stop imei activation over
|
|
|
125 |
// what is only a safety interlock.
|
|
|
126 |
LOGGER.warn("[{}] browser lane file {} unusable ({}) - proceeding with in-jvm serialisation only",
|
|
|
127 |
owner, LOCK_PATH, e.getMessage());
|
|
|
128 |
return new BrowserLane(owner, null, null);
|
|
|
129 |
}
|
|
|
130 |
|
|
|
131 |
long waitedFrom = System.currentTimeMillis();
|
|
|
132 |
while (true) {
|
|
|
133 |
FileLock lock = null;
|
|
|
134 |
try {
|
|
|
135 |
lock = handle.getChannel().tryLock();
|
|
|
136 |
} catch (OverlappingFileLockException e) {
|
|
|
137 |
// Another thread in this jvm holds it. IN_JVM should have prevented this, so it
|
|
|
138 |
// means someone acquired the channel outside this class. Treat as contended.
|
|
|
139 |
lock = null;
|
|
|
140 |
} catch (IOException e) {
|
|
|
141 |
LOGGER.warn("[{}] browser lane lock failed ({}) - proceeding with in-jvm serialisation only",
|
|
|
142 |
owner, e.getMessage());
|
|
|
143 |
closeQuietly(handle);
|
|
|
144 |
return new BrowserLane(owner, null, null);
|
|
|
145 |
}
|
|
|
146 |
|
|
|
147 |
if (lock != null) {
|
|
|
148 |
long waited = System.currentTimeMillis() - waitedFrom;
|
|
|
149 |
if (waited > POLL_MILLIS) {
|
|
|
150 |
LOGGER.info("[{}] browser lane acquired after waiting {}s", owner, waited / 1000);
|
|
|
151 |
}
|
|
|
152 |
return new BrowserLane(owner, handle, lock);
|
|
|
153 |
}
|
|
|
154 |
|
|
|
155 |
if (System.currentTimeMillis() >= deadline) {
|
|
|
156 |
LOGGER.warn("[{}] browser lane held by another process for the whole {} {} wait"
|
|
|
157 |
+ " - not starting a driver", owner, timeout, unit);
|
|
|
158 |
closeQuietly(handle);
|
|
|
159 |
IN_JVM.unlock();
|
|
|
160 |
return null;
|
|
|
161 |
}
|
|
|
162 |
|
|
|
163 |
try {
|
|
|
164 |
Thread.sleep(POLL_MILLIS);
|
|
|
165 |
} catch (InterruptedException e) {
|
|
|
166 |
Thread.currentThread().interrupt();
|
|
|
167 |
closeQuietly(handle);
|
|
|
168 |
IN_JVM.unlock();
|
|
|
169 |
return null;
|
|
|
170 |
}
|
|
|
171 |
}
|
|
|
172 |
}
|
|
|
173 |
|
|
|
174 |
/**
|
|
|
175 |
* Opens the lock file, creating it world-writable on first use.
|
|
|
176 |
*
|
|
|
177 |
* <p>The permissions are the whole point: cron runs as root and tomcat as tomcat, and
|
|
|
178 |
* whichever starts first creates the file. A root-created 0644 file would leave tomcat
|
|
|
179 |
* unable to take a write lock, which fails open and silently gives up the interlock.
|
|
|
180 |
*/
|
|
|
181 |
private static RandomAccessFile open() throws IOException {
|
|
|
182 |
File file = new File(LOCK_PATH);
|
|
|
183 |
boolean fresh = !file.exists();
|
|
|
184 |
RandomAccessFile handle = new RandomAccessFile(file, "rw");
|
|
|
185 |
if (fresh) {
|
|
|
186 |
try {
|
|
|
187 |
Set<PosixFilePermission> perms = EnumSet.of(
|
|
|
188 |
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
|
|
|
189 |
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE,
|
|
|
190 |
PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE);
|
|
|
191 |
Files.setPosixFilePermissions(Paths.get(LOCK_PATH), perms);
|
|
|
192 |
} catch (Exception e) {
|
|
|
193 |
LOGGER.warn("Could not widen permissions on {} - the other jvm may not be able to"
|
|
|
194 |
+ " take this lock: {}", LOCK_PATH, e.getMessage());
|
|
|
195 |
}
|
|
|
196 |
}
|
|
|
197 |
return handle;
|
|
|
198 |
}
|
|
|
199 |
|
|
|
200 |
/** Releases the lane. Idempotent, so it is safe in a finally that also runs on the happy path. */
|
|
|
201 |
@Override
|
|
|
202 |
public void close() {
|
|
|
203 |
if (released) {
|
|
|
204 |
return;
|
|
|
205 |
}
|
|
|
206 |
released = true;
|
|
|
207 |
if (fileLock != null) {
|
|
|
208 |
try {
|
|
|
209 |
fileLock.release();
|
|
|
210 |
} catch (Exception e) {
|
|
|
211 |
LOGGER.warn("[{}] releasing the browser lane lock failed: {}", owner, e.getMessage());
|
|
|
212 |
}
|
|
|
213 |
}
|
|
|
214 |
closeQuietly(handle);
|
|
|
215 |
IN_JVM.unlock();
|
|
|
216 |
}
|
|
|
217 |
|
|
|
218 |
private static void closeQuietly(RandomAccessFile handle) {
|
|
|
219 |
if (handle != null) {
|
|
|
220 |
try {
|
|
|
221 |
handle.close();
|
|
|
222 |
} catch (Exception ignored) {
|
|
|
223 |
}
|
|
|
224 |
}
|
|
|
225 |
}
|
|
|
226 |
}
|