Skip to content

Commit 240e8a9

Browse files
committed
fixing busy waiting in abstraction and eliminate busy-waiting
1 parent ede37bd commit 240e8a9

File tree

3 files changed

+422
-66
lines changed

3 files changed

+422
-66
lines changed

microservices-log-aggregation/src/main/java/com/iluwatar/logaggregation/LogAggregator.java

Lines changed: 141 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@
2323
* THE SOFTWARE.
2424
*/
2525
package com.iluwatar.logaggregation;
26-
26+
import java.util.concurrent.BlockingQueue;
2727
import java.util.concurrent.ConcurrentLinkedQueue;
28-
import java.util.concurrent.ExecutorService;
28+
import java.util.concurrent.LinkedBlockingQueue;
29+
import java.util.concurrent.ScheduledExecutorService;
2930
import java.util.concurrent.Executors;
3031
import java.util.concurrent.TimeUnit;
3132
import java.util.concurrent.atomic.AtomicInteger;
33+
import java.util.concurrent.CountDownLatch;
34+
import java.util.ArrayList;
35+
import java.util.List;
3236
import lombok.extern.slf4j.Slf4j;
3337

3438
/**
@@ -41,11 +45,17 @@
4145
public class LogAggregator {
4246

4347
private static final int BUFFER_THRESHOLD = 3;
48+
private static final int FLUSH_INTERVAL_SECONDS = 5;
49+
private static final int SHUTDOWN_TIMEOUT_SECONDS = 10;
50+
4451
private final CentralLogStore centralLogStore;
4552
private final ConcurrentLinkedQueue<LogEntry> buffer = new ConcurrentLinkedQueue<>();
4653
private final LogLevel minLogLevel;
4754
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
4855
private final AtomicInteger logCount = new AtomicInteger(0);
56+
private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1);
57+
private final CountDownLatch shutdownLatch = new CountDownLatch(1);
58+
private volatile boolean running = true;
4959

5060
/**
5161
* constructor of LogAggregator.
@@ -57,6 +67,15 @@ public LogAggregator(CentralLogStore centralLogStore, LogLevel minLogLevel) {
5767
this.centralLogStore = centralLogStore;
5868
this.minLogLevel = minLogLevel;
5969
startBufferFlusher();
70+
// Add shutdown hook for graceful termination
71+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
72+
try {
73+
stop();
74+
} catch (InterruptedException e) {
75+
LOGGER.warn("Shutdown interrupted", e);
76+
Thread.currentThread().interrupt();
77+
}
78+
}));
6079
}
6180

6281
/**
@@ -65,6 +84,11 @@ public LogAggregator(CentralLogStore centralLogStore, LogLevel minLogLevel) {
6584
* @param logEntry The log entry to collect.
6685
*/
6786
public void collectLog(LogEntry logEntry) {
87+
if (!running) {
88+
LOGGER.warn("LogAggregator is shutting down. Skipping log entry.");
89+
return;
90+
}
91+
6892
if (logEntry.getLevel() == null || minLogLevel == null) {
6993
LOGGER.warn("Log level or threshold level is null. Skipping.");
7094
return;
@@ -75,10 +99,17 @@ public void collectLog(LogEntry logEntry) {
7599
return;
76100
}
77101

78-
buffer.offer(logEntry);
102+
// BlockingQueue.offer() is non-blocking and thread-safe
103+
boolean added = buffer.offer(logEntry);
104+
if (!added) {
105+
LOGGER.warn("Failed to add log entry to buffer - queue may be full");
106+
return;
107+
}
79108

109+
// Check if immediate flush is needed due to threshold
80110
if (logCount.incrementAndGet() >= BUFFER_THRESHOLD) {
81-
flushBuffer();
111+
// Schedule immediate flush instead of blocking current thread
112+
scheduledExecutor.execute(this::flushBuffer);
82113
}
83114
}
84115

@@ -88,32 +119,123 @@ public void collectLog(LogEntry logEntry) {
88119
* @throws InterruptedException If any thread has interrupted the current thread.
89120
*/
90121
public void stop() throws InterruptedException {
91-
executorService.shutdownNow();
92-
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
93-
LOGGER.error("Log aggregator did not terminate.");
122+
LOGGER.info("Stopping LogAggregator...");
123+
running = false;
124+
125+
// Shutdown the scheduler gracefully
126+
scheduledExecutor.shutdown();
127+
128+
try {
129+
// Wait for scheduled tasks to complete
130+
if (!scheduledExecutor.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
131+
LOGGER.warn("Scheduler did not terminate gracefully, forcing shutdown");
132+
scheduledExecutor.shutdownNow();
133+
134+
// Wait a bit more for tasks to respond to interruption
135+
if (!scheduledExecutor.awaitTermination(2, TimeUnit.SECONDS)) {
136+
LOGGER.error("Scheduler did not terminate after forced shutdown");
137+
}
138+
}
139+
} finally {
140+
// Final flush of any remaining logs
141+
flushBuffer();
142+
shutdownLatch.countDown();
143+
LOGGER.info("LogAggregator stopped successfully");
94144
}
95-
flushBuffer();
96145
}
97146

147+
148+
/**
149+
* Waits for the LogAggregator to complete shutdown.
150+
* Useful for testing or controlled shutdown scenarios.
151+
*
152+
* @throws InterruptedException If any thread has interrupted the current thread.
153+
*/
154+
public void awaitShutdown() throws InterruptedException {
155+
shutdownLatch.await();
156+
}
157+
158+
98159
private void flushBuffer() {
99-
LogEntry logEntry;
100-
while ((logEntry = buffer.poll()) != null) {
101-
centralLogStore.storeLog(logEntry);
102-
logCount.decrementAndGet();
160+
if (!running && buffer.isEmpty()) {
161+
return;
162+
}
163+
164+
try {
165+
List<LogEntry> batch = new ArrayList<>();
166+
int drained = 0;
167+
168+
// Drain up to a reasonable batch size for efficiency
169+
LogEntry logEntry;
170+
while ((logEntry = buffer.poll()) != null && drained < 100) {
171+
batch.add(logEntry);
172+
drained++;
173+
}
174+
175+
if (!batch.isEmpty()) {
176+
LOGGER.debug("Flushing {} log entries to central store", batch.size());
177+
178+
// Process the batch
179+
for (LogEntry entry : batch) {
180+
centralLogStore.storeLog(entry);
181+
logCount.decrementAndGet();
182+
}
183+
184+
LOGGER.debug("Successfully flushed {} log entries", batch.size());
185+
}
186+
} catch (Exception e) {
187+
LOGGER.error("Error occurred while flushing buffer", e);
103188
}
104189
}
105190

106-
private void startBufferFlusher() {
107-
executorService.execute(
191+
/**
192+
* Starts the periodic buffer flusher using ScheduledExecutorService.
193+
* This eliminates the busy-waiting loop with Thread.sleep().
194+
*/
195+
private void startPeriodicFlusher() {
196+
scheduledExecutor.scheduleAtFixedRate(
108197
() -> {
109-
while (!Thread.currentThread().isInterrupted()) {
198+
if (running) {
110199
try {
111-
Thread.sleep(5000); // Flush every 5 seconds.
112200
flushBuffer();
113-
} catch (InterruptedException e) {
114-
Thread.currentThread().interrupt();
201+
} catch (Exception e) {
202+
LOGGER.error("Error in periodic flush", e);
115203
}
116204
}
117-
});
205+
},
206+
FLUSH_INTERVAL_SECONDS, // Initial delay
207+
FLUSH_INTERVAL_SECONDS, // Period
208+
TimeUnit.SECONDS
209+
);
210+
211+
LOGGER.info("Periodic log flusher started with interval of {} seconds", FLUSH_INTERVAL_SECONDS);
212+
}
213+
/**
214+
* Gets the current number of buffered log entries.
215+
* Useful for monitoring and testing.
216+
*
217+
* @return Current buffer size
218+
*/
219+
public int getBufferSize() {
220+
return buffer.size();
221+
}
222+
223+
/**
224+
* Gets the current log count.
225+
* Useful for monitoring and testing.
226+
*
227+
* @return Current log count
228+
*/
229+
public int getLogCount() {
230+
return logCount.get();
231+
}
232+
233+
/**
234+
* Checks if the LogAggregator is currently running.
235+
*
236+
* @return true if running, false if stopped or stopping
237+
*/
238+
public boolean isRunning() {
239+
return running;
118240
}
119241
}

server-session/src/main/java/com/iluwatar/sessionserver/App.java

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,18 @@
2424
*/
2525
package com.iluwatar.sessionserver;
2626

27+
28+
import java.util.HashMap;
2729
import com.sun.net.httpserver.HttpServer;
2830
import java.io.IOException;
2931
import java.net.InetSocketAddress;
30-
import java.time.Instant;
31-
import java.util.HashMap;
3232
import java.util.Iterator;
33+
import java.time.Instant;
34+
import java.util.concurrent.ConcurrentHashMap;
35+
import java.util.concurrent.Executors;
36+
import java.util.concurrent.ScheduledExecutorService;
37+
import java.util.concurrent.TimeUnit;
38+
import java.util.concurrent.CountDownLatch;
3339
import java.util.Map;
3440
import lombok.extern.slf4j.Slf4j;
3541

@@ -57,6 +63,12 @@ public class App {
5763
private static Map<String, Instant> sessionCreationTimes = new HashMap<>();
5864
private static final long SESSION_EXPIRATION_TIME = 10000;
5965

66+
// Scheduler for session expiration task
67+
private static ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
68+
private static volatile boolean running = true;
69+
private static final CountDownLatch shutdownLatch = new CountDownLatch(1);
70+
71+
6072
/**
6173
* Main entry point.
6274
*
@@ -78,39 +90,61 @@ public static void main(String[] args) throws IOException {
7890
sessionExpirationTask();
7991

8092
LOGGER.info("Server started. Listening on port 8080...");
93+
// Wait for shutdown signal
94+
try {
95+
shutdownLatch.await();
96+
} catch (InterruptedException e) {
97+
LOGGER.error("Main thread interrupted", e);
98+
Thread.currentThread().interrupt();
99+
}
81100
}
82101

83102
private static void sessionExpirationTask() {
84-
new Thread(
85-
() -> {
86-
while (true) {
87-
try {
88-
LOGGER.info("Session expiration checker started...");
89-
Thread.sleep(SESSION_EXPIRATION_TIME); // Sleep for expiration time
90-
Instant currentTime = Instant.now();
91-
synchronized (sessions) {
92-
synchronized (sessionCreationTimes) {
93-
Iterator<Map.Entry<String, Instant>> iterator =
94-
sessionCreationTimes.entrySet().iterator();
95-
while (iterator.hasNext()) {
96-
Map.Entry<String, Instant> entry = iterator.next();
97-
if (entry
98-
.getValue()
99-
.plusMillis(SESSION_EXPIRATION_TIME)
100-
.isBefore(currentTime)) {
101-
sessions.remove(entry.getKey());
102-
iterator.remove();
103-
}
104-
}
105-
}
106-
}
107-
LOGGER.info("Session expiration checker finished!");
108-
} catch (InterruptedException e) {
109-
LOGGER.error("An error occurred: ", e);
110-
Thread.currentThread().interrupt();
111-
}
112-
}
113-
})
114-
.start();
103+
if (!running) {
104+
return;
105+
}
106+
try {
107+
LOGGER.info("Session expiration checker started...");
108+
Instant currentTime = Instant.now();
109+
110+
// Use removeIf for efficient removal without explicit synchronization
111+
// ConcurrentHashMap handles thread safety internally
112+
sessionCreationTimes.entrySet().removeIf(entry -> {
113+
if (entry.getValue().plusMillis(SESSION_EXPIRATION_TIME).isBefore(currentTime)) {
114+
sessions.remove(entry.getKey());
115+
LOGGER.debug("Expired session: {}", entry.getKey());
116+
return true;
117+
}
118+
return false;
119+
});
120+
121+
LOGGER.info("Session expiration checker finished! Active sessions: {}", sessions.size());
122+
} catch (Exception e) {
123+
LOGGER.error("An error occurred during session expiration check: ", e);
124+
}
125+
}
126+
127+
/**
128+
* Gracefully shuts down the session expiration scheduler.
129+
* This method is called by the shutdown hook.
130+
*/
131+
private static void shutdown() {
132+
LOGGER.info("Shutting down session expiration scheduler...");
133+
running = false;
134+
scheduler.shutdown();
135+
136+
try {
137+
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
138+
LOGGER.warn("Scheduler did not terminate gracefully, forcing shutdown");
139+
scheduler.shutdownNow();
140+
}
141+
} catch (InterruptedException e) {
142+
LOGGER.warn("Shutdown interrupted, forcing immediate shutdown");
143+
scheduler.shutdownNow();
144+
Thread.currentThread().interrupt();
145+
}
146+
147+
shutdownLatch.countDown();
148+
LOGGER.info("Session expiration scheduler shut down complete");
115149
}
116150
}

0 commit comments

Comments
 (0)