Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
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
}